diff --git a/TypedSyntax/src/TypedSyntax.jl b/TypedSyntax/src/TypedSyntax.jl index b9f8e4ed..5798e795 100644 --- a/TypedSyntax/src/TypedSyntax.jl +++ b/TypedSyntax/src/TypedSyntax.jl @@ -11,6 +11,7 @@ using CodeTracking export TypedSyntaxNode include("node.jl") +include("vscode.jl") include("show.jl") end diff --git a/TypedSyntax/src/node.jl b/TypedSyntax/src/node.jl index f843825a..802745e8 100644 --- a/TypedSyntax/src/node.jl +++ b/TypedSyntax/src/node.jl @@ -35,7 +35,8 @@ function tsn_and_mappings(m::Method, src::CodeInfo, @nospecialize(rt); warn::Boo end function tsn_and_mappings(m::Method, src::CodeInfo, @nospecialize(rt), sourcetext::AbstractString, lineno::Integer; warn::Bool=true, strip_macros::Bool=false, kwargs...) - rootnode = JuliaSyntax.parsestmt(SyntaxNode, sourcetext; filename=string(m.file), first_line=lineno, kwargs...) + filename = isnothing(functionloc(m)[1]) ? string(m.file) : functionloc(m)[1] + rootnode = JuliaSyntax.parsestmt(SyntaxNode, sourcetext; filename=filename, first_line=lineno, kwargs...) if strip_macros rootnode = get_function_def(rootnode) if !is_function_def(rootnode) diff --git a/TypedSyntax/src/show.jl b/TypedSyntax/src/show.jl index 1a5c52fb..d1f10672 100644 --- a/TypedSyntax/src/show.jl +++ b/TypedSyntax/src/show.jl @@ -44,7 +44,7 @@ function Base.printstyled(io::IO, rootnode::MaybeTypedSyntaxNode; sig, body = children(rootnode) type_annotate, pre, pre2, post = type_annotation_mode(sig, rt; type_annotations, hide_type_stable) position = show_src_expr(io, sig, position, pre, pre2; type_annotations, iswarn, hide_type_stable, nd) - type_annotate && show_annotation(io, rt, post; iswarn) + type_annotate && show_annotation(io, rt, post, rootnode.source, position; iswarn) rootnode = body end position = show_src_expr(io, rootnode, position, "", ""; type_annotations, iswarn, hide_type_stable, nd) @@ -55,6 +55,14 @@ Base.printstyled(rootnode::MaybeTypedSyntaxNode; kwargs...) = printstyled(stdout ndigits_linenumbers(node::AbstractSyntaxNode, idxend = last_byte(node)) = ndigits(node.source.first_line + nlines(node.source, idxend) - 1) +function _print(io::IO, x, node, position) + print(io, x) + + if !isempty(x) + add_hint!(get(io, :inlay_hints, nothing), x, node, position+1) + end +end + function show_src_expr(io::IO, node::MaybeTypedSyntaxNode, position::Int, pre::String, pre2::String; type_annotations::Bool=true, iswarn::Bool=false, hide_type_stable::Bool=false, nd::Int) _lastidx = last_byte(node) position = catchup(io, node, position, nd) @@ -64,13 +72,13 @@ function show_src_expr(io::IO, node::MaybeTypedSyntaxNode, position::Int, pre::S position = catchup(io, first(children(node)), position, nd) end end - print(io, pre) + _print(io, pre, node.source, position) for (i, child) in enumerate(children(node)) - i == 2 && print(io, pre2) + i == 2 && _print(io, pre2, node.source, position) cT = gettyp(child) ctype_annotate, cpre, cpre2, cpost = type_annotation_mode(child, cT; type_annotations, hide_type_stable) position = show_src_expr(io, child, position, cpre, cpre2; type_annotations, iswarn, hide_type_stable, nd) - ctype_annotate && show_annotation(io, cT, cpost; iswarn) + ctype_annotate && show_annotation(io, cT, cpost, node.source, position; iswarn) end return catchup(io, node, position, nd, _lastidx+1) end @@ -100,19 +108,25 @@ function type_annotation_mode(node, @nospecialize(T); type_annotations::Bool, hi return type_annotate, pre, pre2, post end -function show_annotation(io, @nospecialize(T), post=""; iswarn::Bool) +function show_annotation(io, @nospecialize(T), post, node, position; iswarn::Bool) + diagnostics = get(io, :diagnostics, nothing) + inlay_hints = get(io, :inlay_hints, nothing) + print(io, post) - if iswarn - color = !is_type_unstable(T) ? :cyan : - is_small_union_or_tunion(T) ? :yellow : :red - printstyled(io, "::", T; color) + T_str = string(T) + if iswarn && is_type_unstable(T) + color = is_small_union_or_tunion(T) ? :yellow : :red + printstyled(io, "::", T_str; color) + add_diagnostic!(diagnostics, node, position+1, is_small_union_or_tunion(T) ? DiagnosticKinds.Information : DiagnosticKinds.Warning) + add_hint!(inlay_hints, string(post, "::", T_str), node, position+1; kind=InlayHintKinds.Nothing) else - printstyled(io, "::", T; color=:cyan) + printstyled(io, "::", T_str; color=:cyan) + add_hint!(inlay_hints, string(post, "::", T_str), node, position+1; kind=InlayHintKinds.Type) end end print_linenumber(io::IO, node::MaybeTypedSyntaxNode, position::Int, nd::Int) = - print_linenumber(io, source_line(node.source, position), nd) + print_linenumber(io, source_line(node.source, position+1), nd) print_linenumber(io::IO, ln::Int, nd::Int) = printstyled(io, lpad(ln, nd), " "; color=:light_black) # Do any "overdue" printing, generating a line number if needed. Mostly, this catches whitespace. diff --git a/TypedSyntax/src/vscode.jl b/TypedSyntax/src/vscode.jl new file mode 100644 index 00000000..43ed73e8 --- /dev/null +++ b/TypedSyntax/src/vscode.jl @@ -0,0 +1,91 @@ +diagnostics_available_vscode() = isdefined(Main, :VSCodeServer) && Main.VSCodeServer isa Module && isdefined(Main.VSCodeServer, :DIAGNOSTICS_ENABLED) && Main.VSCodeServer.DIAGNOSTICS_ENABLED[] +inlay_hints_available_vscode() = isdefined(Main, :VSCodeServer) && Main.VSCodeServer isa Module && isdefined(Main.VSCodeServer, :INLAY_HINTS_ENABLED) && Main.VSCodeServer.INLAY_HINTS_ENABLED[] + +module DiagnosticKinds + @enum T Error=0 Warning=1 Information=2 Hint=3 +end + +struct Diagnostic + path::String + line::Int # 1-based indexing + severity::DiagnosticKinds.T + msg::String +end + +to_vscode_type(x::Diagnostic) = (msg=x.msg, path = x.path, line = x.line, severity = Int(x.severity)) +function Base.show(io::IO, ::MIME"application/vnd.julia-vscode.diagnostics", diagnostics::AbstractVector{Diagnostic}) + return ( + source = "Cthulhu", + items = to_vscode_type.(diagnostics), + ) +end + +add_diagnostic!(::Nothing, node, position, severity) = nothing +function add_diagnostic!(diagnostics, node, position, severity) + file_path = node.filename + line = source_line(node, position) + push!(diagnostics, Diagnostic(file_path, line, severity, "Unstable Type")) +end + +function clear_diagnostics_vscode() + if diagnostics_available_vscode() + display(Main.VSCodeServer.InlineDisplay(false), TypedSyntax.Diagnostic[]) + end +end + +function display_diagnostics_vscode(diagnostics) + if diagnostics_available_vscode() && !isnothing(diagnostics) + # InlineDisplay(false) means we don't print to REPL + display(Main.VSCodeServer.InlineDisplay(false), diagnostics) + end +end +display_diagnostics_vscode(io::IO) = display_diagnostics_vscode(get(io, :diagnostics, nothing)) + +const InlayHintKinds = (Type=1, Parameter=2, Nothing=nothing) + +struct InlayHint + line::Int # 1-based indexing + column::Int # 1-based indexing + label::String + kind::Union{Nothing, Int} +end + +to_vscode_type(x::InlayHint) = (position=(x.line, x.column), label=x.label, kind=x.kind) +function Base.show(io::IO, ::MIME"application/vnd.julia-vscode.inlayHints", inlay_hints_by_file::Dict{T, Vector{InlayHint}}) where T + if inlay_hints_available_vscode() + return Dict{T, Vector{NamedTuple{(:position, :label, :kind), Tuple{Tuple{Int, Int}, String, Union{Nothing, Int}}}}}( + filepath => to_vscode_type.(inlay_hints) for (filepath, inlay_hints) in inlay_hints_by_file + ) + end + return nothing +end + +add_hint!(::Nothing, message, node, position; kind=InlayHintKinds.Type) = nothing +function add_hint!(inlay_hints, message, node, position; kind=InlayHintKinds.Type) + filepath = node.filename + line, column = source_location(node, position) + + if filepath ∉ keys(inlay_hints) + inlay_hints[filepath] = InlayHint[] + end + push!(inlay_hints[filepath], InlayHint(line-1, column-1, message, kind)) +end + +function clear_inlay_hints_vscode() + if inlay_hints_available_vscode() + display(Main.VSCodeServer.InlineDisplay(false), Dict{String, Vector{TypedSyntax.InlayHint}}()) + end +end + +function display_inlay_hints_vscode(inlay_hints) + if inlay_hints_available_vscode() && !isnothing(inlay_hints) + # InlineDisplay(false) means we don't print to REPL + display(Main.VSCodeServer.InlineDisplay(false), inlay_hints) + end +end +display_inlay_hints_vscode(io::IO) = display_inlay_hints_vscode(get(io, :inlay_hints, nothing)) + +function clear_all_vscode() + clear_diagnostics_vscode() + clear_inlay_hints_vscode() +end \ No newline at end of file diff --git a/TypedSyntax/test/runtests.jl b/TypedSyntax/test/runtests.jl index 96f0ab1b..ffcd46fe 100644 --- a/TypedSyntax/test/runtests.jl +++ b/TypedSyntax/test/runtests.jl @@ -634,3 +634,24 @@ end if parse(Bool, get(ENV, "CI", "false")) include("exhaustive.jl") end + +module VSCodeServer + struct InlineDisplay + is_repl::Bool + end + const INLAY_HINTS_ENABLED = Ref(true) + const DIAGNOSTICS_ENABLED = Ref(true) + + function Base.display(d::InlineDisplay, x) + return nothing + end +end +module TestVSCodeExt # stops modules defined in test files from overwriting stuff from previous test +using Test, ..VSCodeServer + +@testset "VSCode TypedSyntax.jl" begin + @testset "test_vscode.jl" begin + include("test_vscode.jl") + end +end +end \ No newline at end of file diff --git a/TypedSyntax/test/test_module.jl b/TypedSyntax/test/test_module.jl index 0e530cdc..d9b1c18c 100644 --- a/TypedSyntax/test/test_module.jl +++ b/TypedSyntax/test/test_module.jl @@ -223,4 +223,10 @@ const T426 = Dict{Type{<:Dates.Period}, Bool}( # Issue #458 f458() = return +function fVSCode(x) + z = x + 1 + y = 2 * z + return y + (x > 0 ? -1 : 1.0) +end + end diff --git a/TypedSyntax/test/test_vscode.jl b/TypedSyntax/test/test_vscode.jl new file mode 100644 index 00000000..d64bd7b6 --- /dev/null +++ b/TypedSyntax/test/test_vscode.jl @@ -0,0 +1,668 @@ +module test_vscode + +using ..VSCodeServer +using JuliaSyntax: JuliaSyntax, SyntaxNode, children, child, sourcetext, kind, @K_str +using TypedSyntax: TypedSyntax, TypedSyntaxNode, getsrc, InlayHint, Diagnostic, InlayHintKinds +using Dates, InteractiveUtils, Test + +has_name_typ(node, name::Symbol, @nospecialize(T)) = kind(node) == K"Identifier" && node.val === name && node.typ === T +has_name_notyp(node, name::Symbol) = has_name_typ(node, name, nothing) + +include("test_module.jl") + +@testset "TypedSyntax.jl" begin + specializations(m::Method) = isdefined(Base, :specializations) ? Base.specializations(m) : m.specializations + + st = """ + f(x, y, z) = x * y + z + """ + rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN1.jl") + TSN.eval(Expr(rootnode)) + src, _ = getsrc(TSN.f, (Float32, Int, Float64)) + tsn = TypedSyntaxNode(rootnode, src) + sig, body = children(tsn) + @test children(sig)[2].typ === Float32 + @test children(sig)[3].typ === Int + @test children(sig)[4].typ === Float64 + @test body.typ === Float64 # aggregate output + @test children(body)[1].typ === Float32 + + # Multiline + st = """ + function g(a, b, c) + x = a + b + return x + c + end + """ + rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN2.jl") + TSN.eval(Expr(rootnode)) + src, _ = getsrc(TSN.g, (Int16, Int16, Int32)) + tsn = TypedSyntaxNode(rootnode, src) + sig, body = children(tsn) + @test length(children(sig)) == 4 + @test children(body)[2].typ === Int32 + # Check that `x` gets an assigned type + nodex = child(body, 1, 1) + @test nodex.typ === Int16 + + # Target ambiguity + st = "math(x) = x + sin(x + π / 4)" + rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN2.jl") + TSN.eval(Expr(rootnode)) + src, _ = getsrc(TSN.math, (Int,)) + tsn = TypedSyntaxNode(rootnode, src) + sig, body = children(tsn) + @test has_name_typ(child(body, 1), :x, Int) + @test has_name_typ(child(body, 3, 2, 1), :x, Int) + pi4 = child(body, 3, 2, 3) + @test kind(pi4) == K"call" && pi4.typ == typeof(π / 4) + tsn = TypedSyntaxNode(TSN.has2xa, (Real,)) + @test tsn.typ === Any + sig, body = children(tsn) + @test has_name_typ(child(sig, 2), :x, Real) + @test has_name_typ(child(body, 1, 2), :x, Real) + @test has_name_typ(child(body, 1, 1), :x, Any) + tsn = TypedSyntaxNode(TSN.has2xb, (Real,)) + @test tsn.typ === Any + sig, body = children(tsn) + @test has_name_typ(child(sig, 2), :x, Real) + @test has_name_typ(child(body, 1, 2), :x, Real) + @test has_name_typ(child(body, 1, 1), :x, Any) + + # Target duplication + st = "math2(x) = sin(x) + sin(x)" + rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN2.jl") + TSN.eval(Expr(rootnode)) + src, _ = getsrc(TSN.math2, (Int,)) + tsn = TypedSyntaxNode(rootnode, src) + sig, body = children(tsn) + @test body.typ === Float64 + @test_broken child(body, 1).typ === Float64 + tsn = TypedSyntaxNode(TSN.simplef, Tuple{Float32, Int32}) + sig, body = children(tsn) + @test has_name_typ(child(body, 1, 2, 1), :a, Float32) + @test has_name_typ(child(body, 1, 2, 3), :a, Float32) + + # Inner functions + for (st, idxsinner, idxsouter) in ( + ("firstfirst(c) = map(x -> first(x), first(c))", (2, 2), (3,)), + (""" + firstfirst(c) = map(first(c)) do x + first(x) + end + """, (3, 1), (1, 2)) + ) + rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN3.jl") + TSN.eval(Expr(rootnode)) + src, _ = getsrc(TSN.firstfirst, (Vector{Vector{Real}},)) + tsn = TypedSyntaxNode(rootnode, src) + sig, body = children(tsn) + @test child(body, idxsinner...).typ === nothing + @test child(body, idxsouter...).typ === Vector{Real} + end + + # body macros + tsn = TypedSyntaxNode(TSN.hasmacro, (Tuple{Int,Int}, Char)) + sig, body = children(tsn) + @test has_name_typ(child(body, 2, 2, 2), :t, Tuple{Int,Int}) + @test has_name_typ(child(body, 2, 3), :x, Char) + + # signature macros + tsn = TypedSyntaxNode(TSN.nospec, (Any,)) + sig, body = children(tsn) + node = child(sig, 2) + @test kind(node) == K"macrocall" + @test child(node, 1).val == Symbol("@nospecialize") + @test has_name_typ(child(node, 2), :x, Any) + @test body.typ === Any + tsn = TypedSyntaxNode(TSN.nospec2, (AbstractVecOrMat,)) + sig, body = children(tsn) + node = child(sig, 2) + @test kind(node) == K"macrocall" + @test child(node, 1).val == Symbol("@nospecialize") + arg = child(node, 2) + @test kind(arg) == K"::" + @test has_name_typ(child(arg, 1), :x, AbstractVecOrMat) + @test body.typ === Any + tsn = TypedSyntaxNode(eltype, (TSN.ReadOnly,)) + @test tsn.typ === Type{Int} + + # signature return-type annotations + tsn = TypedSyntaxNode(TSN.withrt, (IO,)) + @test tsn.typ === Bool + sig, body = children(tsn) + @test has_name_typ(child(sig, 1, 2, 1), :io, IO) + tsn = TypedSyntaxNode(TSN.mytimes, (Bool,Float16)) + sig, body = children(tsn) + @test has_name_typ(child(sig, 1, 1, 2, 1), :x, Bool) + @test has_name_typ(child(sig, 1, 1, 3, 1), :y, Float16) + + # operators + tsn = TypedSyntaxNode(+, (TSN.MyInt, TSN.MyInt)) + sig, body = children(tsn) + @test has_name_typ(child(sig, 2, 1), :a, TSN.MyInt) + tsn = TypedSyntaxNode(-, (TSN.MyInt, TSN.MyInt)) + sig, body = children(tsn) + @test has_name_typ(child(sig, 2, 1), :a, TSN.MyInt) + + # `ref` indexing + st = """ + function setlist!(listset, listget, i, j) + listset[i+1][j+1] = listget[i][j] + end + """ + rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN4.jl") + TSN.eval(Expr(rootnode)) + src, rt = getsrc(TSN.setlist!, (Vector{Vector{Float32}}, Vector{Vector{UInt8}}, Int, Int)) + tsn = TypedSyntaxNode(rootnode, src) + sig, body = children(tsn) + nodelist = child(body, 1, 2, 1, 1) # `listget` + @test sourcetext(nodelist) == "listget" && nodelist.typ === Vector{Vector{UInt8}} + @test nodelist.parent.typ === Vector{UInt8} # `listget[i]` + @test sourcetext(child(nodelist.parent, 2)) == "i" + @test nodelist.parent.parent.typ === UInt8 # `listget[i][j]` + + nodelist = child(body, 1, 1, 1, 1) # `listset` + @test sourcetext(nodelist) == "listset" && nodelist.typ === Vector{Vector{Float32}} + @test sourcetext(child(nodelist.parent, 2)) == "i+1" + @test nodelist.parent.typ === Vector{Float32} # `listset[i+1]` + @test kind(nodelist.parent.parent.parent) == K"=" # the `setindex!` call + + # tuple-destructuring + st = """ + function callfindmin(list) + val, idx = findmin(list) + x, y = idx, val + return y + end + """ + rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN5.jl") + TSN.eval(Expr(rootnode)) + src, rt = getsrc(TSN.callfindmin, (Vector{Float64},)) + tsn = TypedSyntaxNode(rootnode, src) + sig, body = children(tsn) + t = child(body, 1, 1) + @test kind(t) == K"tuple" + @test has_name_typ(child(t, 1), :val, Float64) + @test has_name_typ(child(t, 2), :idx, Int) + t = child(body, 2, 1) + @test kind(t) == K"tuple" + @test has_name_typ(child(t, 1), :x, Int) + @test has_name_typ(child(t, 2), :y, Float64) + + tsn = TypedSyntaxNode(TSN.bar381, (TSN.Foo381,)) + sig, body = children(tsn) + lhs = child(body, 1, 1) + @test has_name_typ(child(lhs, 1), :a, Any) + @test has_name_typ(child(lhs, 2, 1), :b1, Any) + @test has_name_typ(child(lhs, 2, 2), :b2, Any) + + tsn = TypedSyntaxNode(TSN.extrema2, (Tuple{Int,Int}, Tuple{Int,Int})) + sig, body = children(tsn) + @test child(sig, 2).typ == Tuple{Int,Int} + + g = Base.Generator(identity, 1.0:4.0) + tsn = TypedSyntaxNode(TSN.typeof_first_item, (typeof(g),)) + sig, body = children(tsn) + @test has_name_typ(child(body, 3, 1, 1), :val, Float64) + + # GlobalRefs + tsn = TypedSyntaxNode(TSN.getchar1, (Int,)) + sig, body = children(tsn) + @test has_name_typ(child(body, 1), :charset1, Any) + tsn = TypedSyntaxNode(TSN.getchar2, (Int,)) + sig, body = children(tsn) + @test has_name_typ(child(body, 1), :charset2, typeof(TSN.charset2)) + + # Generators & comprehensions + tsn = TypedSyntaxNode(TSN.boxedgenerator368, (Int,)) + sig, body = children(tsn) + cnode = child(body, 2) + @test kind(cnode) == K"comprehension" + @test cnode.typ == Vector + tsn = TypedSyntaxNode(TSN.nestedgenerators, (Int, Int)) + sig, body = children(tsn) + @test kind(body) == K"generator" + @test body.typ <: Base.Iterators.Flatten + tsn = TypedSyntaxNode(TSN.nestedgenerators, (Int,)) + sig, body = children(tsn) + @test kind(body) == K"generator" + @test body.typ <: Base.Iterators.Flatten + tsn = TypedSyntaxNode(TSN.nestedexplicit, (Int,)) + sig, body = children(tsn) + @test kind(body) == K"comprehension" + @test body.typ <: Vector + node = child(body, 1) + @test kind(node) == K"generator" + @test node.typ <: Base.Generator + + # Broadcasting + tsn = TypedSyntaxNode(TSN.fbroadcast, (Vector{Int},)) + sig, body = children(tsn) + @test body.typ === Float64 + cnode = child(body, 2) + @test kind(cnode) == K"dotcall" + @test cnode.typ == Vector{Float64} + tsn = TypedSyntaxNode(TSN.fbroadcast_explicit, (Vector{Int},)) + sig, body = children(tsn) + @test body.typ === Float64 + cnode = child(body, 2) + cnodef = child(cnode, 1, 2, 1) + @test kind(cnodef) == K"Identifier" && cnodef.val == :materialize + @test cnode.typ === Vector{Float64} + cnode = child(body, 2, 2) + cnodef = child(cnode, 1, 2, 1) + @test kind(cnodef) == K"Identifier" && cnodef.val == :broadcasted + @test cnode.typ <: Broadcast.Broadcasted + tsn = TypedSyntaxNode(TSN.fbroadcast2, (Vector{Int},)) + sig, body = children(tsn) + node = child(body, 2) + src = tsn.typedsource + if isa(src.code[1], GlobalRef) + @test kind(node) == K"dotcall" && node.typ === Vector{String} + else + # We aren't quite handling this properly yet + @test_broken kind(node) == K"dotcall" && node.typ === Vector{String} + end + tsn = TypedSyntaxNode(TSN.bcast415, (TSN.B415, Float64)) + sig, body = children(tsn) + @test child(body, 1).typ === Float64 + + # Misc lowering + tsn = TypedSyntaxNode(TSN.myunique, (AbstractRange,)) + sig, body = children(tsn) + @test has_name_typ(child(body, 2), :r, AbstractRange) + @test_broken has_name_typ(child(body, 3, 2), :r, AbstractRange) + + # kwfuncs + st = """ + function avoidzero(x; avoid_zero=true) + fx = float(x) + return iszero(x) ? oftype(fx, NaN) : fx + end + """ + rootnode = JuliaSyntax.parsestmt(SyntaxNode, st; filename="TSN6.jl") + TSN.eval(Expr(rootnode)) + src, rt = getsrc(TSN.avoidzero, (Int,)) + # src looks like this: + # %1 = Main.TSN.:(var"#avoidzero#6")(true, #self#, x)::Float64 + # return %1 + # Consequently there is nothing to match, but at least we shouldn't error + tsn = TypedSyntaxNode(rootnode, src) + @test isa(tsn, TypedSyntaxNode) + @test rt === Float64 + # Try the kwbodyfunc + m = which(TSN.avoidzero, (Int,)) + src, rt = getsrc(Base.bodyfunction(m), (Bool, typeof(TSN.avoidzero), Int,)) + tsn = TypedSyntaxNode(rootnode, src) + sig, body = children(tsn) + isz = child(body, 2, 1, 1) + @test kind(isz) == K"call" && child(isz, 1).val == :iszero + @test isz.typ === Bool + @test child(body, 2, 1, 2).typ == Float64 + + # default positional arguments + tsn = TypedSyntaxNode(TSN.defaultarg, (Float32,)) + sig, body = children(tsn) + @test has_name_typ(child(sig, 2), :x, Float32) + @test has_name_notyp(child(sig, 3, 1), :y) + # there is no argument 2 in tsn.typedsource + tsn = TypedSyntaxNode(TSN.defaultarg, (Float32,Int)) + sig, body = children(tsn) + @test has_name_typ(child(sig, 2), :x, Float32) + nodearg = child(sig, 3) + @test kind(nodearg) == K"=" + @test has_name_typ(child(nodearg, 1), :y, Int) + # default position args that are types + tsn = TypedSyntaxNode(TSN.hasdefaulttypearg, (Type{Float32},)) + sig, body = children(tsn) + arg = child(sig, 1, 2, 1) + @test kind(arg) == K"::" && arg.typ === Type{Float32} + tsn = TypedSyntaxNode(TSN.hasdefaulttypearg, ()) + sig, body = children(tsn) + arg = child(sig, 1, 2, 1) + @test_broken kind(arg) == K"::" && arg.typ === Type{Rational{Int}} # maybe this shouldn't even be true + + # macros in function definition + tsn = TypedSyntaxNode(TSN.mysin, (Int,)) + @test kind(tsn) == K"macrocall" + sig, body = children(child(tsn, 2)) + @test has_name_typ(child(sig, 2, 1), :x, Int) + @test has_name_typ(child(body, 1, 1), :xf, Float64) + + # generated functions + # This mostly tests that we don't choke on assignments to variables whose names do not show up in the source + tsn = TypedSyntaxNode(TSN.generated385, (Vector{Int},)) + @test kind(tsn) == K"macrocall" + sig, body = children(child(tsn, 2)) + @test has_name_typ(child(sig, 2, 1), :dest, Vector{Int}) + + # `for` loops + tsn = TypedSyntaxNode(TSN.summer, (Vector{Float64},)) + @test tsn.typ == Union{Int,Float64} + sig, body = children(tsn) + @test has_name_typ(child(sig, 2), :list, Vector{Float64}) + @test has_name_typ(child(body, 1, 1), :s, Int) + @test has_name_typ(child(body, 2, 1, 1), :x, Float64) + node = child(body, 2, 2, 1) + @test kind(node) == K"+=" + @test has_name_typ(child(node, 1), :s, Float64) # if this line runs, the LHS now has type `Float64` + @test has_name_typ(child(node, 2), :x, Float64) + @test has_name_typ(child(body, 3, 1), :s, Union{Float64, Int}) + tsn = TypedSyntaxNode(TSN.summer_iterate, (Vector{Float64},)) + @test tsn.typ == Union{Int,Float64} + sig, body = children(tsn) + @test has_name_typ(child(body, 2, 1), :ret, Union{Nothing, Tuple{Float64, Int64}}) + @test has_name_typ(child(body, 3, 2, 1, 1, 1), :x, Float64) + + # `where`, unnamed arguments, and types-as-arguments + tsn = TypedSyntaxNode(TSN.zerowhere, (Vector{Int16},)) + sig, body = children(tsn) + @test child(sig, 1, 2).typ === Vector{Int16} + @test body.typ === Int16 + @test has_name_typ(child(body, 2), :T, Type{Int16}) + # tsn = TypedSyntaxNode(TSN.vaparam, (Matrix{Float32}, (String, Bool))) # fails on `which` + m = @which TSN.vaparam(rand(3,3), ("hello", false)) + mi = first(specializations(m)) + tsn = TypedSyntaxNode(mi) + sig, body = children(tsn) + @test has_name_typ(child(sig, 1, 3, 1), :I, Tuple{String, Bool}) + tsn = TypedSyntaxNode(TSN.myplustv, (Char, UInt8)) + sig, body = children(TypedSyntax.get_function_def(tsn)) + @test has_name_typ(child(sig, 1, 2, 1), :x, Char) + @test has_name_typ(child(sig, 1, 3, 1), :y, UInt8) + tsn = TypedSyntaxNode(TSN.myplustv, (AbstractChar, UInt8)) + sig, body = children(TypedSyntax.get_function_def(tsn)) + @test has_name_typ(child(sig, 1, 2, 1), :x, AbstractChar) + @test has_name_typ(child(sig, 1, 3, 1), :y, UInt8) + tsn = TypedSyntaxNode(TSN.cb, (Vector{Int16}, Int)) + sig, body = children(tsn) + @test has_name_typ(child(body, 2), :Bool, Type{Bool}) + tsn = TypedSyntaxNode(TSN.unnamedargs, (Type{Matrix{Float32}}, Type{Int})) + sig, body = children(tsn) + m = @which TSN.unnamedargs(Matrix{Float32}, Int, Int) + fbody = Base.bodyfunction(m) + @test child(sig, 1, 2).typ === Type{Matrix{Float32}} + @test child(sig, 1, 3).typ === Type{Int} + m = @which TSN.unnamedargs(Matrix{Float32}, Int; a="hello") + mi = nothing + for _mi in specializations(m) + _mi === nothing && continue + nt = _mi.specTypes.parameters[2] + nt <: NamedTuple || continue + if nt == NamedTuple{(:a,), Tuple{String}} + mi = _mi + break + end + end + tsn = TypedSyntaxNode(mi) + sig, body = children(tsn) + @test child(sig, 1, 2).typ === Type{Matrix{Float32}} + @test child(sig, 1, 3).typ === Type{Int} + @test has_name_notyp(child(sig, 1, 4, 1), :c) + @test has_name_typ(child(sig, 1, 5, 1, 1), :a, String) + m = @which TSN.unnamedargs(Matrix{Float32}, Int, :c; a="hello") + mi = nothing + for _mi in specializations(m) + _mi === nothing && continue + if any(==(Symbol), _mi.specTypes.parameters) + mi = _mi + break + end + end + tsn = TypedSyntaxNode(mi) + sig, body = children(tsn) + @test child(sig, 1, 2).typ === Type{Matrix{Float32}} + @test child(sig, 1, 3).typ === Type{Int} + @test child(sig, 1, 4, 1).typ === Symbol + @test child(sig, 1, 5, 1, 1).typ === String + mbody = only(methods(fbody)) + mi = nothing + for _mi in specializations(mbody) + _mi === nothing && continue + if any(==(Symbol), _mi.specTypes.parameters) + mi = _mi + break + end + end + tsn = TypedSyntaxNode(mi) + sig, body = children(tsn) + @test child(sig, 1, 2).typ === Type{Matrix{Float32}} + @test child(sig, 1, 3).typ === Type{Int} + @test child(sig, 1, 4, 1).typ === Symbol + @test child(sig, 1, 5, 1, 1).typ === String + tsn = TypedSyntaxNode(TSN.unnamedargs2, (Type{Matrix}, Symbol)) + sig, body = children(tsn) + @test child(sig, 2).typ === Type{Matrix} + @test child(sig, 3, 1).typ === Symbol + @test has_name_notyp(child(sig, 4, 1, 1, 1), :padding) + + # Values as static_parameters + tsn = TypedSyntaxNode(TSN.val, (Val{4},)) + sig, body = children(tsn) + @test TypedSyntax.child(body, 1, 1, 1).typ == Core.Const(4) + + # varargs + tsn = TypedSyntaxNode(TSN.likevect, (Int, Int)) + sig, body = children(tsn) + nodeva = child(sig, 1, 2) + @test kind(nodeva) == K"..." + @test has_name_typ(child(nodeva, 1, 1), :X, Tuple{Int,Int}) + tsn = TypedSyntaxNode(TSN.cbva, (Matrix{Float32}, Int, Int)) + sig, body = children(tsn) + @test body.typ === Bool + @test has_name_typ(child(body, 2), :Bool, Type{Bool}) + @test has_name_typ(child(body, 3), :a, Matrix{Float32}) + nodeva = child(body, 4) + @test kind(nodeva) == K"..." + @test has_name_typ(child(nodeva, 1), :i, Tuple{Int,Int}) + tsn = TypedSyntaxNode(TSN.splats, (Tuple{Int,Int}, Tuple{Int})) + sig, body = children(tsn) + @test body.typ === Vector{Int} + @test has_name_typ(child(body, 2, 1), :x, Tuple{Int,Int}) + @test has_name_typ(child(body, 3, 1), :y, Tuple{Int}) + m = @which TSN.anykwargs(; cat=1, dog=2) + mi = first(specializations(m)) + tsn = TypedSyntaxNode(mi) + src = tsn.typedsource + @test Symbol("kwargs...") ∈ src.slotnames + sig, body = children(tsn) + @test child(body, 2, 1).typ <: Base.Iterators.Pairs + + # quoted symbols that could be confused for function definition + tsn = TypedSyntaxNode(TSN.isexpreq, (Expr,)) + sig, body = children(tsn) + @test has_name_typ(child(sig, 2, 1), :ex, Expr) + + # Unused statements + tsn = TypedSyntaxNode(TSN.mycheckbounds, (Vector{Int}, Int)) + @test tsn.typ === Nothing + sig, body = children(tsn) + errnode = child(body, 1, 2) + errf = child(errnode, 1) + @test errnode.typ === nothing && errf.typ === typeof(Base.throw_boundserror) + retnode = child(body, 2) + @test kind(retnode) == K"return" + @test retnode.typ === nothing || retnode.typ === Nothing + + # Globals & scoped assignment + tsn = TypedSyntaxNode(TSN.setglobal, (Char,)) + # Agnostic about whether it's good to tag the type of `myglobal`, but at least `val` should be tagged + sig, body = children(tsn) + @test has_name_typ(child(body, 1, 1, 2), :val, Char) + + # DataTypes + tsn = TypedSyntaxNode(TSN.myoftype, (Float64, Int)) + sig, body = children(tsn) + node = child(body, 1) + @test node.typ === Type{Float64} + tsn = TypedSyntaxNode(TSN.DefaultArray{Float32}, (Vector{Int}, Int)) + sig, body = children(tsn) + @test kind(sig) == K"where" + @test kind(child(sig, 1)) == K"call" + f = child(sig, 1, 1) + @test kind(f) == K"curly" && f.typ === Type{TSN.DefaultArray{Float32}} + + # Field access & a more complex example + tsn = TypedSyntaxNode(Base.getindex, (TSN.DefaultArray{Float64,2,Matrix{Float64}}, Int, Int)) + @test tsn.typ === Float64 + sig, body = children(tsn) + @test kind(body) == K"?" + @test child(body, 1).typ === Bool + nodeidx = child(body, 2) + @test nodeidx.typ === Float64 + @test child(nodeidx, 1).typ === Matrix{Float64} + default = child(body, 3) + @test default.typ === Float64 + @test child(default, 1).typ === TSN.DefaultArray{Float64,2,Matrix{Float64}} + + # global + tsn = TypedSyntaxNode(TSN.in_let, (Int,)) + sig, body = children(child(tsn, 1)) + @test has_name_typ(child(sig, 2), :x, Int) + @test_broken body.typ == Int + + # Construction from MethodInstance + src, rt = TypedSyntax.getsrc(TSN.myoftype, (Float64, Int)) + tsn = TypedSyntaxNode(src.parent) + sig, body = children(tsn) + node = child(body, 1) + @test node.typ === Type{Float64} + + # UnionAll in signature (issue #409) + @static if VERSION ≥ v"1.9-" + tsn = TypedSyntaxNode(Core.kwcall, (NamedTuple, typeof(issorted), Vector{Int})) + sig, body = children(tsn) + @test has_name_typ(child(sig, 2), :itr, Vector{Int}) + end + + # Empty `return` (issue #458) + tsn = TypedSyntaxNode(TSN.f458, ()) + @test tsn.typ === Nothing + sig, body = children(tsn) + @test kind(body) == K"return" + @test body.typ === Nothing + + # Counting arguments (needed for Cthulhu) + # issue #397 + tsn = TypedSyntaxNode(TSN.f397, (typeof(view([1,2,3], 1:2)),)) + @test TypedSyntax.num_positional_args(tsn) == 2 # the function is arg1, x is arg2 + # issue #426 + tsn = TypedSyntaxNode(getindex, (typeof(TSN.T426), Type{Year},)) + @test TypedSyntax.num_positional_args(tsn) == 3 + # issue #433 + tsn = TypedSyntaxNode(TSN.withrt, (IO,)) + @test TypedSyntax.num_positional_args(tsn) == 2 + + # Display + tsn = TypedSyntaxNode(TSN.mysin, (Int,)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + # There are several potential valid ways to print the signature. + # Here we allow them all (some tests below may not be as flexible). + # Arguably the first seems best, as it has the inferred type + # bound more tightly to the object than the type-assertion in the signature, + # and is valid syntax that is a bit more sparing in its use of (). + @test occursin("function (\$f)(x::Int64::Real)::Float64", str) || + occursin("function (\$f)((x::Int64)::Real)::Float64", str) || + occursin("function (\$f)(x::Real::Int64)::Float64", str) || + occursin("function (\$f)((x::Real)::Int64)::Float64", str) + tsn = TypedSyntaxNode(TSN.summer, (Vector{Any},)) + str = sprint(tsn; context=:color=>true) do io, obj + printstyled(io, obj; iswarn=true, hide_type_stable=false) + end + @test occursin("summer(list\e[36m::Vector{Any}\e[39m)\e[31m::Any", str) + @test occursin("s\e[31m::Any\e[39m += x\e[31m::Any\e[39m", str) + str = sprint(tsn; context=:color=>true) do io, obj + printstyled(io, obj; type_annotations=false) + end + @test occursin("summer(list)", str) + @test occursin("s += x", str) + tsn = TypedSyntaxNode(TSN.summer, (Vector{Float64},)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test occursin("s::$Int = 0::$Int", str) + @test !occursin("(s::$Int = 0::$Int)", str) + @test occursin("(s::Float64 += x::Float64)::Float64", str) + tsn = TypedSyntaxNode(TSN.zerowhere, (Vector{Int16},)) + str = sprint(tsn; context=:color=>true) do io, obj + printstyled(io, obj; iswarn=true, hide_type_stable=false) + end + @test occursin("AbstractArray{T})\e[36m::Vector{Int16}\e[39m", str) + @test occursin("where T<:Real)\e[36m::Int16\e[39m", str) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + # One could either have `(::AbstractArray{T})` or `::(AbstractArray{T})` + # The latter is more consistent with how we want `-x` to print. + @test occursin("(zerowhere(::(AbstractArray{T})::Vector{Int16}) where T<:Real)::Int16", str) + @test occursin("zero(T::Type{Int16})", str) + tsn = TypedSyntaxNode(TSN.add2, (Vector{Float32},)) + str = sprint(tsn; context=:color=>true) do io, obj + printstyled(io, obj; iswarn=true, hide_type_stable=false) + end + @test occursin("[1]\e[36m::Float32\e[39m", str) + @test occursin("[2]\e[36m::Float32\e[39m", str) + tsn = TypedSyntaxNode(TSN.simplef, (Int, Float64)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test str === """ + 5 function simplef(a::Int64, b::Float64)::Float64 + 6 z::Int64 = (a::Int64 * a::Int64)::Int64 + 7 return (z::Int64 + b::Float64)::Float64 + 8 end""" + tsn = TypedSyntaxNode(TSN.myabs, (Float64,)) + str = sprint(tsn; context=:color=>false) do io, obj + printstyled(io, obj; hide_type_stable=false) + end + @test occursin("-(x::Float64)::Float64", str) + + # issue #413 + @test TypedSyntax.is_small_union_or_tunion(Union{}) + @test TypedSyntax.is_small_union_or_tunion(Union{Tuple{}, Tuple{Int64}}) + + # artificial trigger for #414 + tsn = TypedSyntaxNode(TSN.unnamedargs, (Type{Matrix{Float32}}, Type{Int})) + sig, body = children(tsn) + @test isa(TypedSyntax.map_signature!(sig, fill(Symbol(""), 3), Any[Any, Core.Const(2), Any]), TypedSyntaxNode) + + # issue #435 + tsnc = copy(tsn) + @test isa(tsnc, TypedSyntaxNode) +end + +@testset "test_vscode.jl" begin + # VSCode + tsn = TypedSyntaxNode(TSN.fVSCode, (Int64,)) + + io = IOContext(devnull, :inlay_hints=>Dict{String, Vector{InlayHint}}(), :diagnostics=>Diagnostic[]) + printstyled(io, tsn) + @test getproperty.(first(values(io[:inlay_hints])), :kind) == [InlayHintKinds.Nothing, InlayHintKinds.Type, InlayHintKinds.Nothing] && getproperty.(first(values(io[:inlay_hints])), :label) == ["::Union{Float64, Int64}", "(", ")::Union{Float64, Int64}"] + @test length(io[:diagnostics]) == 2 + + io = IOContext(devnull, :inlay_hints=>Dict{String, Vector{InlayHint}}(), :diagnostics=>Diagnostic[]) + printstyled(io, tsn; hide_type_stable=false) + @test getproperty.(first(values(io[:inlay_hints])), :kind) == vcat(InlayHintKinds.Type, InlayHintKinds.Nothing, repeat([InlayHintKinds.Type], 15), InlayHintKinds.Nothing) && getproperty.(first(values(io[:inlay_hints])), :label) == ["::Int64" + "::Union{Float64, Int64}" + "::Int64" + "(" + "::Int64" + ")::Int64" + "::Int64" + "(" + "::Int64" + ")::Int64" + "(" + "::Int64" + "(" + "::Int64" + ")::Bool" + "::Int64" + "::Float64" + ")::Union{Float64, Int64}"] + @test length(io[:diagnostics]) == 2 +end +end \ No newline at end of file diff --git a/src/Cthulhu.jl b/src/Cthulhu.jl index 4fade1a0..6376b798 100644 --- a/src/Cthulhu.jl +++ b/src/Cthulhu.jl @@ -36,6 +36,8 @@ Base.@kwdef mutable struct CthulhuConfig inline_cost::Bool = false type_annotations::Bool = true annotate_source::Bool = true # overrides optimize, although the current setting is preserved + inlay_types_vscode::Bool = true + diagnostics_vscode::Bool = true end """ @@ -63,6 +65,8 @@ end - `inline_cost::Bool` Initial state of "inlining costs" toggle. Defaults to `false`. - `type_annotations::Bool` Initial state of "type annnotations" toggle. Defaults to `true`. - `annotate_source::Bool` Initial state of "Source". Defaults to `true`. +- `inlay_types_vscode::Bool` Initial state of "vscode: inlay types" toggle. Defaults to `true` +- `diagnostics_vscode::Bool` Initial state of "Vscode: diagnostics" toggle. Defaults to `true` """ const CONFIG = CthulhuConfig() @@ -206,6 +210,7 @@ function __descend_with_error_handling(args...; terminal=default_terminal(), kwa try _descend(terminal, args...; kwargs...) catch x + TypedSyntax.clear_all_vscode() if x isa InterruptException return nothing else @@ -385,6 +390,8 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs inline_cost::Bool = CONFIG.inline_cost&CONFIG.optimize, # default is false type_annotations::Bool = CONFIG.type_annotations, # default is true annotate_source::Bool = CONFIG.annotate_source, # default is true + inlay_types_vscode::Bool = CONFIG.inlay_types_vscode, # default is true + diagnostics_vscode::Bool = CONFIG.diagnostics_vscode, # default is true ) if isnothing(hide_type_stable) @@ -406,7 +413,7 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs do_typeinf!(new_interp, new_mi) _descend(term, new_interp, new_mi; debuginfo, optimize, interruptexc, iswarn, hide_type_stable, remarks, - with_effects, inline_cost, type_annotations, annotate_source) + with_effects, inline_cost, type_annotations, annotate_source, inlay_types_vscode, diagnostics_vscode) end custom_toggles = Cthulhu.custom_toggles(interp) if !(custom_toggles isa Vector{CustomToggle}) @@ -472,9 +479,9 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs :with_effects => with_effects) stringify(ioctx) do lambda_io cthulhu_typed(lambda_io, debuginfo, annotate_source ? codeinf : src, rt, effects, mi; - iswarn, hide_type_stable, + iswarn, optimize, hide_type_stable, pc2remarks, pc2effects, - inline_cost, type_annotations, annotate_source, + inline_cost, type_annotations, annotate_source, inlay_types_vscode, diagnostics_vscode, interp) end end @@ -489,9 +496,9 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs :SOURCE_SLOTNAMES => Base.sourceinfo_slotnames(codeinf), :with_effects => with_effects) cthulhu_typed(lambda_io, debuginfo, src, rt, effects, mi; - iswarn, hide_type_stable, + iswarn, optimize, hide_type_stable, pc2remarks, pc2effects, - inline_cost, type_annotations, annotate_source, + inline_cost, type_annotations, annotate_source, inlay_types_vscode, diagnostics_vscode, interp) end view_cmd = cthulhu_typed @@ -503,7 +510,7 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs shown_callsites = annotate_source ? sourcenodes : callsites menu = CthulhuMenu(shown_callsites, with_effects, optimize & !annotate_source, iswarn&get(iostream, :color, false)::Bool, hide_type_stable, custom_toggles; menu_options...) - usg = usage(view_cmd, annotate_source, optimize, iswarn, hide_type_stable, debuginfo, remarks, with_effects, inline_cost, type_annotations, CONFIG.enable_highlighter, custom_toggles) + usg = usage(view_cmd, annotate_source, optimize, iswarn, hide_type_stable, debuginfo, remarks, with_effects, inline_cost, type_annotations, CONFIG.enable_highlighter, inlay_types_vscode, diagnostics_vscode, custom_toggles) cid = request(term, usg, menu) toggle = menu.toggle @@ -592,7 +599,7 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs override = get_override(info), debuginfo, optimize, interruptexc, iswarn, hide_type_stable, - remarks, with_effects, inline_cost, type_annotations, annotate_source) + remarks, with_effects, inline_cost, type_annotations, annotate_source, inlay_types_vscode, diagnostics_vscode) elseif toggle === :warn iswarn ⊻= true @@ -600,6 +607,12 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs with_effects ⊻= true elseif toggle === :hide_type_stable hide_type_stable ⊻= true + elseif toggle === :inlay_types_vscode + inlay_types_vscode ⊻= true + TypedSyntax.clear_inlay_hints_vscode() + elseif toggle === :diagnostics_vscode + diagnostics_vscode ⊻= true + TypedSyntax.clear_diagnostics_vscode() elseif toggle === :optimize optimize ⊻= true if !is_cached(get_mi(curs)) @@ -689,6 +702,8 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs end println(iostream) end + + TypedSyntax.clear_all_vscode() end function do_typeinf!(interp::AbstractInterpreter, mi::MethodInstance) diff --git a/src/codeview.jl b/src/codeview.jl index 8678490e..3486b9b3 100644 --- a/src/codeview.jl +++ b/src/codeview.jl @@ -101,11 +101,12 @@ is_type_unstable(@nospecialize(type)) = type isa Type && (!Base.isdispatchelem(t cthulhu_warntype(args...; kwargs...) = cthulhu_warntype(stdout::IO, args...; kwargs...) function cthulhu_warntype(io::IO, debuginfo::AnyDebugInfo, src::Union{CodeInfo,IRCode}, @nospecialize(rt), effects::Effects, mi::Union{Nothing,MethodInstance}=nothing; - hide_type_stable::Bool=false, inline_cost::Bool=false, interp::CthulhuInterpreter=CthulhuInterpreter()) + hide_type_stable::Bool=false, inline_cost::Bool=false, optimize::Bool=false, + interp::CthulhuInterpreter=CthulhuInterpreter()) if inline_cost isa(mi, MethodInstance) || error("Need a MethodInstance to show inlining costs. Call `cthulhu_typed` directly instead.") end - cthulhu_typed(io, debuginfo, src, rt, effects, mi; iswarn=true, hide_type_stable, inline_cost, interp) + cthulhu_typed(io, debuginfo, src, rt, effects, mi; iswarn=true, optimize, hide_type_stable, inline_cost, interp) return nothing end @@ -121,9 +122,10 @@ cthulhu_typed(io::IO, debuginfo::DebugInfo, args...; kwargs...) = cthulhu_typed(io, Symbol(debuginfo), args...; kwargs...) function cthulhu_typed(io::IO, debuginfo::Symbol, src::Union{CodeInfo,IRCode}, @nospecialize(rt), effects::Effects, mi::Union{Nothing,MethodInstance}; - iswarn::Bool=false, hide_type_stable::Bool=false, + iswarn::Bool=false, hide_type_stable::Bool=false, optimize::Bool=true, pc2remarks::Union{Nothing,PC2Remarks}=nothing, pc2effects::Union{Nothing,PC2Effects}=nothing, inline_cost::Bool=false, type_annotations::Bool=true, annotate_source::Bool=false, + inlay_types_vscode::Bool=false, diagnostics_vscode::Bool=false, interp::AbstractInterpreter=CthulhuInterpreter()) debuginfo = IRShow.debuginfo(debuginfo) @@ -144,7 +146,45 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, if src.slottypes === nothing @warn "Inference terminated in an incomplete state due to argument-type changes during recursion" end - printstyled(lambda_io, tsn; type_annotations, iswarn, hide_type_stable, idxend) + + diagnostics_vscode &= iswarn # If warnings are off then no diagnostics are shown + # Check if diagnostics are avaiable and if mi is defined in a file + if !TypedSyntax.diagnostics_available_vscode() || isnothing(functionloc(mi)[1]) + diagnostics_vscode = false + end + if !TypedSyntax.inlay_hints_available_vscode() || isnothing(functionloc(mi)[1]) + inlay_types_vscode = false + end + + vscode_io = IOContext( + lambda_io, + :inlay_hints => inlay_types_vscode ? Dict{String,Vector{TypedSyntax.InlayHint}}() : nothing , + :diagnostics => diagnostics_vscode ? TypedSyntax.Diagnostic[] : nothing + ) + + if istruncated + printstyled(lambda_io, tsn; type_annotations, iswarn, hide_type_stable, idxend) + else + printstyled(vscode_io, tsn; type_annotations, iswarn, hide_type_stable, idxend) + end + + callsite_diagnostics = TypedSyntax.Diagnostic[] + if (diagnostics_vscode || inlay_types_vscode) + vscode_io = IOContext(devnull, :inlay_hints=>vscode_io[:inlay_hints], :diagnostics=>vscode_io[:diagnostics]) + callsite_mis = Dict() # type annotation is a bit long so I skipped it, doesn't seem to affect performance + visited_mis = Set{MethodInstance}((mi,)) + add_callsites!(callsite_mis, visited_mis, callsite_diagnostics, mi; optimize, annotate_source, interp) + for callsite in values(callsite_mis) + if !isnothing(callsite) + descend_into_callsite!(vscode_io, callsite.tsn; iswarn, hide_type_stable, type_annotations) + end + end + end + + !isnothing(vscode_io[:diagnostics]) && append!(callsite_diagnostics, vscode_io[:diagnostics]) + TypedSyntax.display_diagnostics_vscode(callsite_diagnostics) + TypedSyntax.display_inlay_hints_vscode(vscode_io) + println(lambda_io) istruncated && @info "This method only fills in default arguments; descend into the body method to see the full source." return nothing @@ -276,6 +316,78 @@ function cthulhu_typed(io::IO, debuginfo::Symbol, return nothing end +function descend_into_callsite!(io::IO, tsn::TypedSyntaxNode; + iswarn::Bool, hide_type_stable::Bool, type_annotations::Bool) + sig, body = children(tsn) + # We empty the body when filling kwargs + istruncated = isempty(children(body)) + idxend = istruncated ? JuliaSyntax.last_byte(sig) : lastindex(tsn.source) + if !istruncated # If method only fills in default arguments + printstyled(io, tsn; type_annotations, iswarn, hide_type_stable, idxend) + end +end + +function add_callsites!(d::AbstractDict, visited_mis::AbstractSet, diagnostics::AbstractVector, + mi::MethodInstance, source_mi::MethodInstance=mi; + optimize::Bool=true, annotate_source::Bool=false, + interp::AbstractInterpreter=CthulhuInterpreter()) + + callsites, src, rt = try + (; src, rt, infos, slottypes, effects, codeinf) = lookup(interp, mi, optimize & !annotate_source) + + src = preprocess_ci!(src, mi, optimize & !annotate_source, CONFIG) + if (optimize & !annotate_source) || isa(src, IRCode) # optimization might have deleted some statements + infos = src.stmts.info + else + @assert length(src.code) == length(infos) + end + + # We pass false as it doesn't affect callsites and skips fetching the method definition + # using CodeTracking which is slow + callsites, _ = find_callsites(interp, src, infos, mi, slottypes, optimize & !annotate_source, false) + callsites, src, rt + catch + return nothing + end + + for callsite in callsites + callsite_mi = callsite.info isa MultiCallInfo ? nothing : get_mi(callsite) + + if !isnothing(callsite_mi) && callsite_mi ∉ visited_mis + push!(visited_mis, callsite_mi) + add_callsites!(d, visited_mis, diagnostics, callsite_mi, source_mi; optimize, annotate_source, interp) + end + end + + # Check if callsite is not just filling in default arguments and defined in same file as source_mi + if mi == source_mi || mi.def.file != source_mi.def.file + return nothing + end + tsn, _ = get_typed_sourcetext(mi, src, rt; warn=false) + isnothing(tsn) && return nothing + sig, body = children(tsn) + # We empty the body when filling kwargs + istruncated = isempty(children(body)) + istruncated && return nothing + # We add new callsites unless we would have multiple callsites for the same source definition, + # e.g. if f(x) = x is called with different types we print nothing. + key = (mi.def.file, mi.def.line) + if haskey(d, key) + if !isnothing(d[key]) && mi != d[key].mi + d[key] = nothing + push!(diagnostics, + TypedSyntax.Diagnostic( + isnothing(functionloc(mi)[1]) ? string(mi.file) : functionloc(mi)[1], mi.def.line, + TypedSyntax.DiagnosticKinds.Information, + "Cthulhu disabled: This function was called multiple times with different argument types" + ) + ) + end + else + d[key] = (mi=mi, tsn=tsn) + end +end + @static if VERSION >= v"1.10.0-DEV.552" using Core.Compiler: VarState sptypes(sparams) = VarState[VarState.(sparams, false)...] @@ -352,7 +464,7 @@ function Base.show( return end println(io, "Cthulhu.Bookmark (world: ", world, ")") - cthulhu_typed(io, debuginfo, CI, rt, effects, b.mi; iswarn, hide_type_stable, b.interp) + cthulhu_typed(io, debuginfo, CI, rt, effects, b.mi; iswarn, optimize, hide_type_stable, b.interp) end function InteractiveUtils.code_typed(b::Bookmark; optimize::Bool=true) @@ -383,7 +495,7 @@ function InteractiveUtils.code_warntype( CI, rt = InteractiveUtils.code_typed(b; kw...) (; interp, mi) = b (; effects) = lookup(interp, mi, optimize) - cthulhu_warntype(io, debuginfo, CI, rt, effects, b.mi; hide_type_stable, b.interp) + cthulhu_warntype(io, debuginfo, CI, rt, effects, b.mi; optimize, hide_type_stable, b.interp) end InteractiveUtils.code_llvm(b::Bookmark) = InteractiveUtils.code_llvm(stdout::IO, b) diff --git a/src/preferences.jl b/src/preferences.jl index d7bb895e..ea750f44 100644 --- a/src/preferences.jl +++ b/src/preferences.jl @@ -36,6 +36,8 @@ function save_config!(config::CthulhuConfig=CONFIG) "inline_cost" => config.inline_cost, "type_annotations" => config.type_annotations, "annotate_source" => config.annotate_source, + "inlay_types_vscode" => config.inlay_types_vscode, + "diagnostics_vscode" => config.diagnostics_vscode, ) end @@ -53,4 +55,6 @@ function read_config!(config::CthulhuConfig) config.inline_cost = @load_preference("inline_cost", config.inline_cost) config.type_annotations = @load_preference("type_annotations", config.type_annotations) config.annotate_source = @load_preference("annotate_source", config.annotate_source) + config.inlay_types_vscode = @load_preference("inlay_types_vscode", config.inlay_types_vscode) + config.diagnostics_vscode = @load_preference("diagnostics_vscode", config.diagnostics_vscode) end diff --git a/src/ui.jl b/src/ui.jl index 46419545..3fd52fbd 100644 --- a/src/ui.jl +++ b/src/ui.jl @@ -92,7 +92,7 @@ function stringify(@nospecialize(f), context::IOContext) end const debugcolors = (:nothing, :light_black, :yellow) -function usage(@nospecialize(view_cmd), annotate_source, optimize, iswarn, hide_type_stable, debuginfo, remarks, with_effects, inline_cost, type_annotations, highlight, +function usage(@nospecialize(view_cmd), annotate_source, optimize, iswarn, hide_type_stable, debuginfo, remarks, with_effects, inline_cost, type_annotations, highlight, inlay_types_vscode, diagnostics_vscode, custom_toggles::Vector{CustomToggle}) colorize(use_color::Bool, c::Char) = stringify() do io use_color ? printstyled(io, c; color=:cyan) : print(io, c) @@ -107,6 +107,14 @@ function usage(@nospecialize(view_cmd), annotate_source, optimize, iswarn, hide_ colorize(hide_type_stable, 'h'), "]ide type-stable statements, [", colorize(type_annotations, 't'), "]ype annotations, [", colorize(highlight, 's'), "]yntax highlight for Source/LLVM/Native") + if TypedSyntax.inlay_hints_available_vscode() + print(ioctx, ", [", + colorize(inlay_types_vscode, 'v'), "]scode: inlay types") + end + if TypedSyntax.diagnostics_available_vscode() + print(ioctx, ", [", + colorize(diagnostics_vscode, 'V'), "]scode: diagnostics") + end if !annotate_source print(ioctx, ", [", colorize(optimize, 'o'), "]ptimize, [", @@ -160,6 +168,8 @@ const TOGGLES = Dict( UInt32('b') => :bookmark, UInt32('R') => :revise, UInt32('E') => :edit, + UInt32('v') => :inlay_types_vscode, + UInt32('V') => :diagnostics_vscode ) function TerminalMenus.keypress(m::CthulhuMenu, key::UInt32) diff --git a/test/runtests.jl b/test/runtests.jl index 019f0623..f32cadaa 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -26,3 +26,60 @@ using Test, PerformanceTestTools include("test_AbstractInterpreter.jl") end end + +module VSCodeServer + using TypedSyntax + + struct InlineDisplay + is_repl::Bool + end + const INLAY_HINTS_ENABLED = Ref(true) + const DIAGNOSTICS_ENABLED = Ref(true) + + inlay_hints = [] + diagnostics = [] + + function Base.display(d::InlineDisplay, x) + if x isa Dict{String, Vector{TypedSyntax.InlayHint}} + push!(inlay_hints, x) + elseif eltype(x) == TypedSyntax.Diagnostic + push!(diagnostics, x) + end + return nothing + end + + function reset_test_containers() + empty!(inlay_hints) + empty!(diagnostics) + end +end +module TestVSCodeExt # stops modules defined in test files from overwriting stuff from previous test +using Test, PerformanceTestTools, ..VSCodeServer +@testset "runtests.jl VSCodeExt" begin + @testset "test_Cthulhu.jl" begin + include("test_Cthulhu.jl") + end + + @testset "test_codeview.jl" begin + include("test_codeview.jl") + include("test_codeview_vscode.jl") + end + + # TODO enable this test on nightly + if false + @testset "test_irshow.jl" begin + include("test_irshow.jl") + end + else + @info "skipped test_irshow.jl" + end + + @testset "test_terminal.jl" begin + include("test_terminal.jl") + end + + @testset "test_AbstractInterpreter.jl" begin + include("test_AbstractInterpreter.jl") + end +end +end \ No newline at end of file diff --git a/test/setup.jl b/test/setup.jl index 8b421d24..90d9c067 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -1,4 +1,7 @@ using Test, Cthulhu, InteractiveUtils +if isdefined(parentmodule(@__MODULE__), :VSCodeServer) + using ..VSCodeServer +end function cthulhu_info(@nospecialize(f), @nospecialize(TT=()); optimize=true) (interp, mi) = Cthulhu.mkinterp(Core.Compiler.NativeInterpreter(), f, TT) diff --git a/test/test_AbstractInterpreter.jl b/test/test_AbstractInterpreter.jl index 2b281812..04024942 100644 --- a/test/test_AbstractInterpreter.jl +++ b/test/test_AbstractInterpreter.jl @@ -1,6 +1,9 @@ module test_AbstractInterpreter using Test, Cthulhu +if isdefined(parentmodule(@__MODULE__), :VSCodeServer) + using ..VSCodeServer +end const CC = Core.Compiler import Core: MethodInstance, CodeInstance diff --git a/test/test_codeview_vscode.jl b/test/test_codeview_vscode.jl new file mode 100644 index 00000000..b6eeb309 --- /dev/null +++ b/test/test_codeview_vscode.jl @@ -0,0 +1,279 @@ +module test_codeview_vscode + +using Cthulhu, Test, Revise, REPL, ..VSCodeServer, TypedSyntax +import TypedSyntax: InlayHintKinds + +include("test_vscode_example_functions.jl") + +@testset "VSCode descend test" begin + if !isdefined(@__MODULE__, :fake_terminal) + @eval (@__MODULE__) begin + includet(@__MODULE__, normpath(@__DIR__, "FakeTerminals.jl")) # FIXME change to include + using .FakeTerminals + end + end + + function equal_upto_ordering(x, y) + if length(x) != length(y) + return false + end + + while length(x) > 0 + element = pop!(x) + found_idx = findfirst(x->x==element, y) + if !isnothing(found_idx) + deleteat!(y, found_idx) + else + return false + end + end + return true + end + + for inlay_types_vscode in (true, false), diagnostics_vscode in (true, false), iswarn in (true, false), hide_type_stable in (true, false) + @testset "fib inlay_types_vscode=$inlay_types_vscode, diagnostics_vscode=$diagnostics_vscode, iswarn=$iswarn, hide_type_stable=$hide_type_stable" begin + VSCodeServer.reset_test_containers() + + fake_terminal() do term, in, out, _ + t = @async begin + @test_nowarn descend(fib, (Int64,); terminal=term, iswarn, hide_type_stable, inlay_types_vscode, diagnostics_vscode) + end + write(in, 'q') + wait(t) + end + + if inlay_types_vscode + @test length(VSCodeServer.inlay_hints) == 2 + @test isempty(VSCodeServer.inlay_hints[2]) + else + @test length(VSCodeServer.inlay_hints) == 1 + @test isempty(VSCodeServer.inlay_hints[1]) + end + + if diagnostics_vscode + @test length(VSCodeServer.diagnostics) == 2 + @test isempty(VSCodeServer.diagnostics[2]) + if !iswarn + @test isempty(VSCodeServer.diagnostics[1]) + end + end + + if !hide_type_stable && inlay_types_vscode + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(1, 14, "::Int64", 1) + TypedSyntax.InlayHint(1, 15, "::Int64", 1) + TypedSyntax.InlayHint(3, 11, "(", 1) + TypedSyntax.InlayHint(3, 15, "(", 1) + TypedSyntax.InlayHint(3, 16, "::Int64", 1) + TypedSyntax.InlayHint(3, 20, ")::Int64", 1) + TypedSyntax.InlayHint(3, 21, "::Int64", 1) + TypedSyntax.InlayHint(3, 28, "(", 1) + TypedSyntax.InlayHint(3, 29, "::Int64", 1) + TypedSyntax.InlayHint(3, 33, ")::Int64", 1) + TypedSyntax.InlayHint(3, 34, "::Int64", 1) + TypedSyntax.InlayHint(3, 34, ")::Int64", 1) + ]) + elseif hide_type_stable && inlay_types_vscode + @test isempty(VSCodeServer.inlay_hints[1]) + end + end + end + + for inlay_types_vscode in (true, false), diagnostics_vscode in (true, false), iswarn in (true, false), hide_type_stable in (true, false) + @testset "fVSCode inlay_types_vscode=$inlay_types_vscode, diagnostics_vscode=$diagnostics_vscode, iswarn=$iswarn, hide_type_stable=$hide_type_stable" begin + VSCodeServer.reset_test_containers() + + fake_terminal() do term, in, out, _ + t = @async begin + @test_nowarn descend(fVSCode, (Int64,); terminal=term, iswarn, hide_type_stable, inlay_types_vscode, diagnostics_vscode) + end + write(in, 'q') + wait(t) + end + + if inlay_types_vscode + @test length(VSCodeServer.inlay_hints) == 2 + @test isempty(VSCodeServer.inlay_hints[2]) + else + @test length(VSCodeServer.inlay_hints) == 1 + @test isempty(VSCodeServer.inlay_hints[1]) + end + + if diagnostics_vscode + @test length(VSCodeServer.diagnostics) == 2 + @test isempty(VSCodeServer.diagnostics[2]) + if iswarn + @test equal_upto_ordering(getproperty.(VSCodeServer.diagnostics[1], :line), [8, 11]) + @test getproperty.(VSCodeServer.diagnostics[1], :severity) == [TypedSyntax.DiagnosticKinds.Information, TypedSyntax.DiagnosticKinds.Information] + @test getproperty.(VSCodeServer.diagnostics[1], :msg) == ["Unstable Type", "Unstable Type"] + else + @test isempty(VSCodeServer.diagnostics[1]) + end + end + + if !hide_type_stable && inlay_types_vscode + if iswarn + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(7, 18, "::Int64", 1) + TypedSyntax.InlayHint(7, 19, "::Union{Float64, Int64}", nothing) + TypedSyntax.InlayHint(8, 5, "::Int64", 1) + TypedSyntax.InlayHint(8, 8, "(", 1) + TypedSyntax.InlayHint(8, 9, "::Int64", 1) + TypedSyntax.InlayHint(8, 13, ")::Int64", 1) + TypedSyntax.InlayHint(9, 5, "::Int64", 1) + TypedSyntax.InlayHint(9, 8, "(", 1) + TypedSyntax.InlayHint(9, 13, "::Int64", 1) + TypedSyntax.InlayHint(9, 13, ")::Int64", 1) + TypedSyntax.InlayHint(10, 11, "(", 1) + TypedSyntax.InlayHint(10, 12, "::Int64", 1) + TypedSyntax.InlayHint(10, 16, "(", 1) + TypedSyntax.InlayHint(10, 17, "::Int64", 1) + TypedSyntax.InlayHint(10, 21, ")::Bool", 1) + TypedSyntax.InlayHint(10, 26, "::Int64", 1) + TypedSyntax.InlayHint(10, 32, "::Float64", 1) + TypedSyntax.InlayHint(10, 33, ")::Union{Float64, Int64}", nothing) + ]) + + else + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(7, 18, "::Int64", 1) + TypedSyntax.InlayHint(7, 19, "::Union{Float64, Int64}", 1) + TypedSyntax.InlayHint(8, 5, "::Int64", 1) + TypedSyntax.InlayHint(8, 8, "(", 1) + TypedSyntax.InlayHint(8, 9, "::Int64", 1) + TypedSyntax.InlayHint(8, 13, ")::Int64", 1) + TypedSyntax.InlayHint(9, 5, "::Int64", 1) + TypedSyntax.InlayHint(9, 8, "(", 1) + TypedSyntax.InlayHint(9, 13, "::Int64", 1) + TypedSyntax.InlayHint(9, 13, ")::Int64", 1) + TypedSyntax.InlayHint(10, 11, "(", 1) + TypedSyntax.InlayHint(10, 12, "::Int64", 1) + TypedSyntax.InlayHint(10, 16, "(", 1) + TypedSyntax.InlayHint(10, 17, "::Int64", 1) + TypedSyntax.InlayHint(10, 21, ")::Bool", 1) + TypedSyntax.InlayHint(10, 26, "::Int64", 1) + TypedSyntax.InlayHint(10, 32, "::Float64", 1) + TypedSyntax.InlayHint(10, 33, ")::Union{Float64, Int64}", 1) + ]) + end + elseif hide_type_stable && inlay_types_vscode + if iswarn + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(7, 19, "::Union{Float64, Int64}", nothing) + TypedSyntax.InlayHint(10, 11, "(", 1) + TypedSyntax.InlayHint(10, 33, ")::Union{Float64, Int64}", nothing) + ]) + else + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(7, 19, "::Union{Float64, Int64}", 1) + TypedSyntax.InlayHint(10, 11, "(", 1) + TypedSyntax.InlayHint(10, 33, ")::Union{Float64, Int64}", 1) + ]) + end + end + end + end + + for inlay_types_vscode in (true, false), diagnostics_vscode in (true, false), iswarn in (true, false), hide_type_stable in (true, false) + @testset "fibcall Float64 inlay_types_vscode=$inlay_types_vscode, diagnostics_vscode=$diagnostics_vscode, iswarn=$iswarn, hide_type_stable=$hide_type_stable" begin + VSCodeServer.reset_test_containers() + + fake_terminal() do term, in, out, _ + t = @async begin + @test_nowarn descend(fibcall, (Float64,); terminal=term, iswarn, hide_type_stable, inlay_types_vscode, diagnostics_vscode) + end + write(in, 'q') + wait(t) + end + + if inlay_types_vscode + @test length(VSCodeServer.inlay_hints) == 2 + @test isempty(VSCodeServer.inlay_hints[2]) + else + @test length(VSCodeServer.inlay_hints) == 1 + @test isempty(VSCodeServer.inlay_hints[1]) + end + + if inlay_types_vscode || (diagnostics_vscode && iswarn) + @test length(VSCodeServer.diagnostics) == 2 + @test isempty(VSCodeServer.diagnostics[2]) + @test length(VSCodeServer.diagnostics[1]) == 1 + @test VSCodeServer.diagnostics[1][1].severity == TypedSyntax.DiagnosticKinds.Information + @test VSCodeServer.diagnostics[1][1].line == 2 + @test VSCodeServer.diagnostics[1][1].msg == "Cthulhu disabled: This function was called multiple times with different argument types" + end + + if !hide_type_stable && inlay_types_vscode + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(14, 18, "::Float64", 1) + TypedSyntax.InlayHint(14, 19, "::Int64", 1) + TypedSyntax.InlayHint(15, 13, "::Type{Int64}", 1) + TypedSyntax.InlayHint(15, 15, "::Float64", 1) + TypedSyntax.InlayHint(15, 16, "::Int64", 1) + TypedSyntax.InlayHint(16, 9, "::Float64", 1) + TypedSyntax.InlayHint(16, 10, "::Int64", 1) + ]) + elseif hide_type_stable && inlay_types_vscode + @test isempty(VSCodeServer.inlay_hints[1]) + end + end + end + + for inlay_types_vscode in (true, false), diagnostics_vscode in (true, false), iswarn in (true, false), hide_type_stable in (true, false) + @testset "fibcall Int64 inlay_types_vscode=$inlay_types_vscode, diagnostics_vscode=$diagnostics_vscode, iswarn=$iswarn, hide_type_stable=$hide_type_stable" begin + VSCodeServer.reset_test_containers() + + fake_terminal() do term, in, out, _ + t = @async begin + @test_nowarn descend(fibcall, (Int64,); terminal=term, iswarn, hide_type_stable, inlay_types_vscode, diagnostics_vscode) + end + write(in, 'q') + wait(t) + end + + if inlay_types_vscode + @test length(VSCodeServer.inlay_hints) == 2 + @test isempty(VSCodeServer.inlay_hints[2]) + else + @test length(VSCodeServer.inlay_hints) == 1 + @test isempty(VSCodeServer.inlay_hints[1]) + end + + if diagnostics_vscode + @test length(VSCodeServer.diagnostics) == 2 + @test isempty(VSCodeServer.diagnostics[2]) + if !iswarn + @test isempty(VSCodeServer.diagnostics[1]) + end + end + + if !hide_type_stable && inlay_types_vscode + @test equal_upto_ordering(first(values(VSCodeServer.inlay_hints[1])), [ + TypedSyntax.InlayHint(14, 18, "::Int64", 1) + TypedSyntax.InlayHint(14, 19, "::Int64", 1) + TypedSyntax.InlayHint(15, 13, "::Type{Int64}", 1) + TypedSyntax.InlayHint(15, 15, "::Int64", 1) + TypedSyntax.InlayHint(15, 16, "::Int64", 1) + TypedSyntax.InlayHint(16, 9, "::Int64", 1) + TypedSyntax.InlayHint(16, 10, "::Int64", 1) + TypedSyntax.InlayHint(1, 14, "::Int64", 1) + TypedSyntax.InlayHint(1, 15, "::Int64", 1) + TypedSyntax.InlayHint(3, 11, "(", 1) + TypedSyntax.InlayHint(3, 15, "(", 1) + TypedSyntax.InlayHint(3, 16, "::Int64", 1) + TypedSyntax.InlayHint(3, 20, ")::Int64", 1) + TypedSyntax.InlayHint(3, 21, "::Int64", 1) + TypedSyntax.InlayHint(3, 28, "(", 1) + TypedSyntax.InlayHint(3, 29, "::Int64", 1) + TypedSyntax.InlayHint(3, 33, ")::Int64", 1) + TypedSyntax.InlayHint(3, 34, "::Int64", 1) + TypedSyntax.InlayHint(3, 34, ")::Int64", 1) + ]) + elseif hide_type_stable && inlay_types_vscode + @test isempty(VSCodeServer.inlay_hints[1]) + end + end + end +end + +end \ No newline at end of file diff --git a/test/test_terminal.jl b/test/test_terminal.jl index b4b67091..60bef4a2 100644 --- a/test/test_terminal.jl +++ b/test/test_terminal.jl @@ -1,6 +1,9 @@ module test_terminal using Test, REPL, Cthulhu, Revise +if isdefined(parentmodule(@__MODULE__), :VSCodeServer) + using ..VSCodeServer +end if !isdefined(@__MODULE__, :fake_terminal) @eval (@__MODULE__) begin diff --git a/test/test_vscode_example_functions.jl b/test/test_vscode_example_functions.jl new file mode 100644 index 00000000..8de22280 --- /dev/null +++ b/test/test_vscode_example_functions.jl @@ -0,0 +1,18 @@ +# exact location in file matters +function fib(n) + if n <= 1 return 1 end + return fib(n - 1) + fib(n - 2) +end + +# exact location in file matters +function fVSCode(x) + z = x + 1 + y = 2 * z + return y + (x > 0 ? -1 : 1.0) +end + +# exact location in file matters +function fibcall(n) + fib(Int64(n)) + fib(n) +end \ No newline at end of file