diff --git a/.JETLSConfig.toml b/.JETLSConfig.toml index 339e75f45..0b5b7df60 100644 --- a/.JETLSConfig.toml +++ b/.JETLSConfig.toml @@ -18,14 +18,6 @@ match_type = "regex" severity = "off" path = "LSP/src/**/*.jl" -# This is type instability within JSON3.jl -[[diagnostic.patterns]] -pattern = "no matching method found `convert(.+)`" -match_by = "message" -match_type = "regex" -severity = "off" -path = "src/testrunner/testrunner-types.jl" - # Treat all other diagnostics as warnings (for early detection via jetls check) [[diagnostic.patterns]] pattern = ".*" diff --git a/LSP/Project.toml b/LSP/Project.toml index e508a4c77..d6ab5bb32 100644 --- a/LSP/Project.toml +++ b/LSP/Project.toml @@ -7,11 +7,13 @@ authors = ["Shuhei Kadowaki "] projects = ["test"] [deps] -JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" +Preferences = "21216c6a-2e73-6563-6e65-726566657250" +StructUtils = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" [compat] -JSON3 = "1.14.3" +JSON = "1.5" PrecompileTools = "1.3.3" -StructTypes = "1.11.0" +Preferences = "1.5.0" +StructUtils = "2.6.0" diff --git a/LSP/README.md b/LSP/README.md index 17ccf1a3e..d4533a599 100644 --- a/LSP/README.md +++ b/LSP/README.md @@ -15,7 +15,7 @@ to faithfully translate the TypeScript LSP specification into idiomatic Julia co - Uses `Union{Nothing, Type} = nothing` field to represent TypeScript's optional properties (`field?: Type`) - Supports inheritance through `@extends` to compose interfaces (similar to TypeScript's `extends`) - Enables anonymous interface definitions within `Union` types for inline type specifications - - Automatically configures `StructTypes.omitempties()` to omit optional fields during JSON serialization + - Integrates with JSON.jl v1 and StructUtils.jl for JSON serialization/deserialization - Creates method dispatchers for `RequestMessage` and `NotificationMessage` types to enable LSP message routing - **`@namespace` macro**: Creates Julia modules containing typed constants that correspond to TypeScript `namespace`s: diff --git a/LSP/src/DSL/interface.jl b/LSP/src/DSL/interface.jl index 897c62307..158d20fa7 100644 --- a/LSP/src/DSL/interface.jl +++ b/LSP/src/DSL/interface.jl @@ -1,5 +1,7 @@ const _interface_defs_ = Dict{Symbol,Expr}() +const _debug_ = Dict{Symbol,Pair{Expr,Dict{Symbol,Union{Nothing,Pair{Expr,Any}}}}}() + """ @interface InterfaceName [@extends ParentInterface] begin field::Type @@ -13,6 +15,9 @@ definitions from the LSP specification, featuring: - Optional fields: Use `Union{Nothing, Type} = nothing` to represent TypeScript's optional properties (`field?: Type`) - Interface inheritance: Use `@extends` to compose interfaces from parent interfaces (similar to TypeScript's `extends`) - Anonymous interfaces: Can be used inline within field type declarations (e.g., `Union{Nothing, @interface begin ... end}`) +- JSON serialization: Integrates with JSON.jl v1 and StructUtils.jl for automatic JSON serialization/deserialization +- Automatic type choosing: For fields with `Union` types, automatically generates `StructUtils.fieldtags` implementations + that inspect the JSON structure to determine the correct type during deserialization - Method dispatching: For interfaces extending `RequestMessage` or `NotificationMessage`, automatically registers the message type in the `method_dispatcher` dictionary for LSP message routing @@ -61,6 +66,23 @@ end When a child interface defines a field with the same name as a parent interface, the child's definition takes precedence. +# Automatic type choosing for JSON deserialization + +For fields with `Union` types, `@interface` automatically generates `StructUtils.fieldtags` +implementations that analyze the JSON structure to determine the correct type during +deserialization. This enables automatic conversion of polymorphic fields common in the LSP +specification, e.g.: +```julia +@interface Hover begin + contents::Union{MarkedString, Vector{MarkedString}, MarkupContent} + range::Union{Nothing, Range} = nothing +end + +@interface WorkspaceEdit begin + documentChanges::Union{Nothing, Vector{Union{TextDocumentEdit, CreateFile, RenameFile, DeleteFile}}} = nothing +end +``` + # LSP message dispatching For interfaces that extend `RequestMessage` or `NotificationMessage`, a `method::String` @@ -110,25 +132,24 @@ macro interface(exs...) is_method_dispatchable = false structbody = Expr(:block, __source__) - omittable_fields = Set{Symbol}() extended_fields = Dict{Symbol,Vector{Int}}() duplicated_fields = Int[] if extends !== nothing for extend in extends is_method_dispatchable |= extend === :RequestMessage || extend === :NotificationMessage add_extended_interface!(toplevelblk, structbody, - omittable_fields, extended_fields, duplicated_fields, extend, + __module__, __source__) end end _, method = process_interface_def!(toplevelblk, structbody, - omittable_fields, extended_fields, duplicated_fields, defex, + __module__, __source__, Name) @@ -145,72 +166,57 @@ macro interface(exs...) end function process_interface_def!(toplevelblk::Expr, structbody::Expr, - omittable_fields::Set{Symbol}, extended_fields::Dict{Symbol,Vector{Int}}, duplicated_fields::Vector{Int}, defex::Expr, + __module__::Module, __source__::LineNumberNode, Name::Union{Symbol,Nothing}) - method = _process_interface_def!(toplevelblk, structbody, omittable_fields, extended_fields, duplicated_fields, defex, __source__) - deleteat!(structbody.args, duplicated_fields) is_anon = Name === nothing if is_anon # Name = Symbol("AnonymousInterface", string(__source__)) # XXX this doesn't work probably due to Julia internal bug Name = Symbol("AnonymousInterface", gensym()) end + method, fieldtags_ex = _process_interface_def!( + toplevelblk, structbody, extended_fields, + duplicated_fields, defex, __module__, __source__; + struct_name = Name) + deleteat!(structbody.args, duplicated_fields) structdef = Expr(:struct, false, Name, structbody) + # TODO Use `StructUtils.@defaults` here? kwdef = Expr(:macrocall, GlobalRef(Base, Symbol("@kwdef")), __source__, structdef) # `@kwdef` will attach `Core.__doc__` automatically push!(toplevelblk.args, kwdef) - if !isempty(omittable_fields) - omitempties = Tuple(omittable_fields) - if Name === :InitializeParams - # HACK: In the write->read roundtrip of `InitializationRequest` in the - # `withserver` test, empty `workspaceFolders` needs to be serialized without - # being omitted. So override the `omitempties` for `InitializeParams`. - # In the normal lifecycle of a language server, `InitializeParams` is never - # serialized, so adding this hack doesn't affect the language server's behavior - # (except in the tests) - omitempties = () - end - push!(toplevelblk.args, :(StructTypes.omitempties(::Type{$Name}) = $omitempties)) - end if is_anon push!(toplevelblk.args, :(Base.convert(::Type{$Name}, nt::NamedTuple) = $Name(; nt...))) end - if !is_anon - push!(toplevelblk.args, :($(GlobalRef(@__MODULE__, :_interface_defs_))[$(QuoteNode(Name))] = $(QuoteNode(structbody)))) + push!(toplevelblk.args, :($(GlobalRef(@__MODULE__, :_interface_defs_))[$(QuoteNode(Name))] = $(QuoteNode(structbody)))) + if fieldtags_ex !== nothing + push!(toplevelblk.args, fieldtags_ex) end return Name, method end -function add_extended_interface!(toplevelblk::Expr, structbody::Expr, - omittable_fields::Set{Symbol}, - extended_fields::Dict{Symbol,Vector{Int}}, - duplicated_fields::Vector{Int}, - extend::Symbol, - __source__::LineNumberNode) - return _process_interface_def!(toplevelblk, structbody, - omittable_fields, - extended_fields, - duplicated_fields, - _interface_defs_[extend], - __source__; - extending = true) +function add_extended_interface!( + toplevelblk::Expr, structbody::Expr, extended_fields::Dict{Symbol,Vector{Int}}, + duplicated_fields::Vector{Int}, extend::Symbol, __module__::Module, __source__::LineNumberNode + ) + _process_interface_def!( + toplevelblk, structbody, extended_fields, + duplicated_fields, _interface_defs_[extend], __module__, __source__; + extending = true) + nothing end -function _process_interface_def!(toplevelblk::Expr, structbody::Expr, - omittable_fields::Set{Symbol}, - extended_fields::Dict{Symbol,Vector{Int}}, - duplicated_fields::Vector{Int}, - defex::Expr, - __source__::LineNumberNode; - extending::Bool = false) +function _process_interface_def!( + toplevelblk::Expr, structbody::Expr, extended_fields::Dict{Symbol,Vector{Int}}, + duplicated_fields::Vector{Int}, defex::Expr, __module__::Module, __source__::LineNumberNode; + extending::Bool = false, struct_name::Union{Nothing,Symbol} = nothing, + ) @assert Meta.isexpr(defex, :block) extended_idxs = Int[] method = nothing for i = 1:length(defex.args) - defarg = defex.args[i] - fieldline = defarg + fieldline = defex.args[i] if fieldline isa LineNumberNode || fieldline isa String if fieldline isa LineNumberNode __source__ = fieldline @@ -234,75 +240,193 @@ function _process_interface_def!(toplevelblk::Expr, structbody::Expr, error("Unsupported syntax found in `@interface`: ", defex) end end - omittable = false if Meta.isexpr(fieldline, :(=)) - fielddecl, default = fieldline.args - if Meta.isexpr(fielddecl, :(::)) - fieldname = fielddecl.args[1] + fdecl, default = fieldline.args + if Meta.isexpr(fdecl, :(::)) + fname = fdecl.args[1] else - fieldname = fielddecl + fname = fdecl end - fieldname isa Symbol || error("Invalid `@interface` syntax: ", defex) - if fieldname === :method + fname isa Symbol || error("Invalid `@interface` syntax: ", defex) + if fname === :method default isa String || error("Invalid message definition: ", defex) method isa String && error("Duplicated method definition: ", defex) method = default end else - fielddecl = fieldline + fdecl = fieldline end - if Meta.isexpr(fielddecl, :(::)) - fieldname = fielddecl.args[1] - fieldtype = fielddecl.args[2] - if Meta.isexpr(fieldtype, :curly) && fieldtype.args[1] === :Union - for i = 2:length(fieldtype.args) - ufty = fieldtype.args[i] - if ufty === :Nothing - omittable = true - end - end - end - if Meta.isexpr(fieldtype, :curly) - for i = 1:length(fieldtype.args) - ufty = fieldtype.args[i] + if Meta.isexpr(fdecl, :(::)) + fname = fdecl.args[1] + ftype = fdecl.args[2] + if Meta.isexpr(ftype, :curly) + for i = 1:length(ftype.args) + ufty = ftype.args[i] if Meta.isexpr(ufty, :macrocall) && ufty.args[1] === Symbol("@interface") anon_defex = ufty.args[end] Meta.isexpr(anon_defex, :block) || error("Invalid `@interface` syntax: ", ufty) - fieldtype.args[i] = process_anon_interface_def!(toplevelblk, anon_defex, __source__) + ftype.args[i] = process_anon_interface_def!(toplevelblk, anon_defex, __module__, __source__) end end - elseif Meta.isexpr(fieldtype, :macrocall) && fieldtype.args[1] === Symbol("@interface") - anon_defex = fieldtype.args[end] - Meta.isexpr(anon_defex, :block) || error("Invalid `@interface` syntax: ", fieldtype) - fielddecl.args[2] = process_anon_interface_def!(toplevelblk, anon_defex, __source__) + elseif Meta.isexpr(ftype, :macrocall) && ftype.args[1] === Symbol("@interface") + anon_defex = ftype.args[end] + Meta.isexpr(anon_defex, :block) || error("Invalid `@interface` syntax: ", ftype) + fdecl.args[2] = process_anon_interface_def!(toplevelblk, anon_defex, __module__, __source__) end else - fieldname = fielddecl + fname = fdecl end - fieldname isa Symbol || error("Invalid `@interface` syntax: ", defex) - if omittable - push!(omittable_fields, fieldname) - end - if haskey(extended_fields, fieldname) - append!(duplicated_fields, extended_fields[fieldname]) - omittable || delete!(omittable_fields, fieldname) + fname isa Symbol || error("Invalid `@interface` syntax: ", defex) + if haskey(extended_fields, fname) + append!(duplicated_fields, extended_fields[fname]) end push!(structbody.args, fieldline) if extending push!(extended_idxs, length(structbody.args)) - extended_fields[fieldname] = copy(extended_idxs) + extended_fields[fname] = copy(extended_idxs) empty!(extended_idxs) end end - return method + if struct_name !== nothing + fieldtags_ex = :(let + ntparams = Expr(:parameters) + body = Expr(:tuple, ntparams) + choosetypes = Dict{Symbol,Union{Nothing,Pair{Expr,Any}}}() + for fname in fieldnames($struct_name) + choosefunc_ex = $gen_choosetype_impl($struct_name, fname) + if choosefunc_ex !== nothing + let choosetypefunc = Core.eval($__module__, choosefunc_ex) + # `fname = (; json = (; choosetype = choosetypefunc))` + push!(ntparams.args, Expr(:kw, fname, + Expr(:tuple, Expr(:parameters, Expr(:kw, :json, + Expr(:tuple, Expr(:parameters, Expr(:kw, :choosetype, choosetypefunc)))))))) + choosetypes[fname] = Pair{Expr,Any}(choosefunc_ex, choosetypefunc) + end + else + choosetypes[fname] = nothing + end + end + if !isempty(ntparams.args) + Core.eval($__module__, :($StructUtils.fieldtags(::$StructUtils.StructStyle, ::Type{$$struct_name}) = $body)) + end + if $LSP_DEV_MODE + $_debug_[$(QuoteNode(struct_name))] = body => choosetypes + end + end) + else + fieldtags_ex = nothing + end + return method, fieldtags_ex +end + +# This function is executed _after_ the type definition generated using `@interface`, and +# generatesan implementation of `StructUtils.choose_type` for custom type JSON +# deserialization for the fields of that type definition. Therefore, the code generated by +# `@interface` contains not the code generated by this function itself, but the call to this +# function (and also includes `Core.eval` which is the driver for code execution using it). +# This design is intentional, as accurate type dispatch generation requires the use of +# reflection such as `fieldtype`, and also needs to handle anonymous interface types +# that are potentially included in `@interface` definitions, making it difficult to perform +# this transformation completely symbolically, leading to the design of calling this +# function after the actual type definition. +function gen_choosetype_impl(StructType::Type, fname::Symbol) + ftyp = fieldtype(StructType, fname) + ftyp isa Union || return nothing + choosetypebody = Expr(:block) + lvname = gensym(:lv) + vname = gensym(:x) + push!(choosetypebody.args, :($vname = $lvname[])) + cond_and_rets = Pair{Any,Any}[] + res = add_choosetype_branch!(cond_and_rets, vname, ftyp) + iszero(res & REQUIRES_CHOOSETYPE) && return nothing # JSON.jl shuold handle this field automatically + errmsg = "Uncovered field type: $fname::$ftyp in $StructType" + for (cond, ret) in cond_and_rets + push!(choosetypebody.args, Expr(:(&&), cond, ret)) + end + push!(choosetypebody.args, :(error($errmsg))) + return Expr(:->, :($lvname::JSON.LazyValue), choosetypebody) +end + +const JSONJL_NOTHING = 1<<0 +const JSONJL_PRIMITIVE = 1<<1 +const REQUIRES_CHOOSETYPE_IF_UNION = 1<<2 +const REQUIRES_CHOOSETYPE = 1<<3 +const CONTAINS_PARAMETRIC_DICT = 1<<4 + +function add_choosetype_branch!(cond_and_rets, vname::Symbol, @nospecialize ftyp) + if ftyp isa Union + abit = add_choosetype_branch!(cond_and_rets, vname, ftyp.a) + bbit = add_choosetype_branch!(cond_and_rets, vname, ftyp.b) + if !iszero(abit & CONTAINS_PARAMETRIC_DICT) && !iszero(bbit & CONTAINS_PARAMETRIC_DICT) + error("`@interface` declaration with multiple Dict types unsupported") + end + abbit = abit | bbit + if !iszero(abit & REQUIRES_CHOOSETYPE_IF_UNION) && !iszero(bbit & REQUIRES_CHOOSETYPE_IF_UNION) + # e.g. require `choosetype` for `Union{MyType1,MyType2}`, `Union{Union{Nothing,MyType1},MyType2}` + abbit |= REQUIRES_CHOOSETYPE + end + if (abbit & (JSONJL_PRIMITIVE | REQUIRES_CHOOSETYPE_IF_UNION)) == (JSONJL_PRIMITIVE | REQUIRES_CHOOSETYPE_IF_UNION) + # e.g. require `choosetype` for `Union{String,MyType2}` + abbit |= REQUIRES_CHOOSETYPE + end + return abbit + end + if ftyp <: Vector + ftyp = ftyp::DataType + cond_and_rets′ = Pair{Any,Any}[] + a1name = gensym(:a1) + eltype = ftyp.parameters[1] + add_choosetype_branch!(cond_and_rets′, a1name, eltype) + elcond = reduce(cond_and_rets′; init=false) do @nospecialize(acc), (cond, _)::Pair{Any,Any} + Expr(:(||), acc, cond) + end + elcond === false && error(lazy"Unexpected parameterized vector type found in field declaration: $ftyp") + push!(cond_and_rets, Pair{Any,Any}( + :($vname isa Vector{Any} && + (isempty($vname) || let $a1name = first($vname); $elcond; end)), + :(return $ftyp))) + return REQUIRES_CHOOSETYPE_IF_UNION + elseif ftyp <: Dict + push!(cond_and_rets, Pair{Any,Any}(:($vname isa JSON.Object), :(return $ftyp))) + return REQUIRES_CHOOSETYPE_IF_UNION | CONTAINS_PARAMETRIC_DICT + elseif ftyp === Nothing + push!(cond_and_rets, Pair{Any,Any}(:($vname isa Nothing), :(return Nothing))) + return JSONJL_NOTHING + elseif ftyp in (Any, Bool, Int, UInt, String) + push!(cond_and_rets, Pair{Any,Any}(:($vname isa $ftyp), :(return $ftyp))) + return JSONJL_PRIMITIVE + elseif ftyp === Null + # NOTE This should be `pushfirst!` to consider the case where `Nothing` is in a `Union`. + # When `Nothing` and `Null` coexist, at the point this callback is called, it is confirmed + # that `null` has been explicitly called as the field value, and in that case we should return `null` + pushfirst!(cond_and_rets, Pair{Any,Any}(:($vname isa Nothing), :(return $Null))) + return REQUIRES_CHOOSETYPE_IF_UNION + elseif ftyp === URI + # NOTE This should be `pushfirst!` to consider the case where `Nothing` is in a `Union`. + # When `Nothing` and `Null` coexist, at the point this callback is called, it is confirmed + # that `null` has been explicitly called as the field value, and in that case we should return `null` + pushfirst!(cond_and_rets, Pair{Any,Any}(:($vname isa String), :(return $URI))) + return REQUIRES_CHOOSETYPE_IF_UNION + end + condex = let ftyp=ftyp + reduce(fieldnames(ftyp); init=true) do @nospecialize(acc), fname::Symbol + if typeintersect(fieldtype(ftyp, fname), Nothing) === Nothing + return acc # omittable + end + Expr(:(&&), acc, :(haskey($vname, $(QuoteNode(fname))))) + end + end + push!(cond_and_rets, Pair{Any,Any}(:($vname isa JSON.Object && $condex), :(return $ftyp))) + return REQUIRES_CHOOSETYPE_IF_UNION end -function process_anon_interface_def!(toplevelblk::Expr, defex::Expr, __source__::LineNumberNode) # Anonymous @interface - omittable_fields = Set{Symbol}() +function process_anon_interface_def!( # Anonymous @interface + toplevelblk::Expr, defex::Expr, __module__::Module, __source__::LineNumberNode + ) extended_fields = Dict{Symbol,Vector{Int}}() duplicated_fields = Int[] res, _ = process_interface_def!(toplevelblk, Expr(:block), - omittable_fields, extended_fields, duplicated_fields, defex, __source__, #=Name=#nothing) + extended_fields, duplicated_fields, defex, __module__, __source__, #=Name=#nothing) if !(isempty(extended_fields) && isempty(duplicated_fields)) error("`Anonymous @interface` does not support extension", defex) end diff --git a/LSP/src/LSP.jl b/LSP/src/LSP.jl index f6af51a43..2bec47c65 100644 --- a/LSP/src/LSP.jl +++ b/LSP/src/LSP.jl @@ -1,13 +1,32 @@ module LSP -using StructTypes: StructTypes +using StructUtils: StructUtils +using JSON: JSON + +using Preferences: Preferences +const LSP_DEV_MODE = Preferences.@load_preference("LSP_DEV_MODE", false) include("URIs2/URIs2.jl") using ..URIs2: URI const exports = Set{Symbol}() + const method_dispatcher = Dict{String,DataType}() +# NOTE `Null` and `URI` are referenced directly from interface.jl, so it should be defined before that. + +""" +A special object representing `null` value. +When used as a field that might be omitted in the serialized JSON (i.e. the field can be `nothing`), +the key-value pair appears as `null` instead of being omitted. +This special object is specifically intended for use in `ResponseMessage`. +""" +StructUtils.@nonstruct struct Null end +const null = Null() +Base.show(io::IO, ::Null) = print(io, "null") +StructUtils.lower(::Null) = JSON.Null() +push!(exports, :Null, :null) + include("DSL/interface.jl") include("DSL/namespace.jl") diff --git a/LSP/src/URIs2/URIs2.jl b/LSP/src/URIs2/URIs2.jl index 3ec36efa1..3d2038ec7 100644 --- a/LSP/src/URIs2/URIs2.jl +++ b/LSP/src/URIs2/URIs2.jl @@ -7,7 +7,7 @@ module URIs2 export @uri_str, URI, filename2uri, filepath2uri, isunsavedfile, isunsaveduri, uri2filename, uri2filepath -using StructTypes: StructTypes +using StructUtils: StructUtils include("vendored-from-uris.jl") @@ -22,7 +22,7 @@ Details of a Unified Resource Identifier. - query::Union{Nothing, String} - fragment::Union{Nothing, String} """ -struct URI +StructUtils.@nonstruct struct URI scheme::Union{String,Nothing} authority::Union{String,Nothing} path::String @@ -72,10 +72,9 @@ else end end -Base.convert(::Type{URI}, s::AbstractString) = URI(s) - -# This overload requires `URI(::AbstractString)` as well, which is defined later -StructTypes.StructType(::Type{URI}) = StructTypes.StringType() +# Tell StructUtils how to serialize/deserialize URI as a string +StructUtils.lower(uri::URI) = string(uri) +StructUtils.lift(::Type{URI}, s::String) = URI(s) function percent_decode(str::AbstractString) return unescapeuri(str) diff --git a/LSP/src/base-protocol.jl b/LSP/src/base-protocol.jl index 1b15e916a..b8d184a28 100644 --- a/LSP/src/base-protocol.jl +++ b/LSP/src/base-protocol.jl @@ -1,18 +1,6 @@ # Base types # ========= -""" -A special object representing `null` value. -When used as a field specified as `StructTypes.omitempties`, the key-value pair is not -omitted in the serialized JSON but instead appears as `null`. -This special object is specifically intended for use in `ResponseMessage`. -""" -struct Null end -const null = Null() -StructTypes.StructType(::Type{Null}) = StructTypes.CustomStruct() -StructTypes.lower(::Null) = nothing -push!(exports, :Null, :null) - const boolean = Bool const string = String diff --git a/LSP/src/communication.jl b/LSP/src/communication.jl index b3b71e98f..b50f089ea 100644 --- a/LSP/src/communication.jl +++ b/LSP/src/communication.jl @@ -1,5 +1,3 @@ -using JSON3: JSON3 - """ Endpoint @@ -115,19 +113,17 @@ function read_transport_layer(io::IO) return String(read(io, message_length)) end -const Parsed = @NamedTuple{method::Union{Nothing,String}} - function to_lsp_object(msg_str::AbstractString) - parsed = JSON3.read(msg_str, Parsed) - parsed_method = parsed.method - if parsed_method !== nothing - if haskey(method_dispatcher, parsed_method) - return JSON3.read(msg_str, method_dispatcher[parsed_method]) + lazyjson = JSON.lazy(msg_str) + if hasproperty(lazyjson, :method) + method = lazyjson.method[] + if method isa String && haskey(method_dispatcher, method) + return JSON.parse(lazyjson, method_dispatcher[method]) end - return JSON3.read(msg_str, Dict{Symbol,Any}) + return JSON.parse(lazyjson, Dict{Symbol,Any}) end # TODO Parse response message? - return JSON3.read(msg_str, Dict{Symbol,Any}) + return JSON.parse(lazyjson, Dict{Symbol,Any}) end writelsp(io::IO, @nospecialize msg) = write_transport_layer(io, to_lsp_json(msg)) @@ -141,7 +137,7 @@ function write_transport_layer(io::IO, response::String) return n end -to_lsp_json(@nospecialize msg) = JSON3.write(msg) +to_lsp_json(@nospecialize msg) = JSON.json(msg; omit_null=true) function Base.close(endpoint::Endpoint) put!(endpoint.out_msg_queue, nothing) # send a special token to terminate the write task diff --git a/LSP/src/precompile.jl b/LSP/src/precompile.jl index d509333d3..1e4008d82 100644 --- a/LSP/src/precompile.jl +++ b/LSP/src/precompile.jl @@ -1,8 +1,8 @@ function test_roundtrip(f, s::AbstractString, Typ) - x = JSON3.read(s, Typ) + x = JSON.parse(s, Typ) f(x) s′ = to_lsp_json(x) - x′ = JSON3.read(s′, Typ) + x′ = JSON.parse(s′, Typ) f(x′) end diff --git a/LSP/test/runtests.jl b/LSP/test/runtests.jl index 2ca8d29c7..d3e921b52 100644 --- a/LSP/test/runtests.jl +++ b/LSP/test/runtests.jl @@ -13,7 +13,7 @@ using Test "result": null }""", DefinitionResponse) do res @test res isa DefinitionResponse - @test_broken res.result === null # this null should be preserved through the roundtrip + @test res.result === null # this null should be preserved through the roundtrip end test_roundtrip("""{ "jsonrpc": "2.0", diff --git a/src/testrunner/testrunner.jl b/src/testrunner/testrunner.jl index 2696e8d13..d6bcc1233 100644 --- a/src/testrunner/testrunner.jl +++ b/src/testrunner/testrunner.jl @@ -538,7 +538,7 @@ function _testrunner_run_testset( end try - LSP.JSON3.read(testrunnerproc, TestRunnerResult) + LSP.JSON.parse(testrunnerproc, TestRunnerResult) catch err @error "Error from testrunner process" err show_error_message(server, """ @@ -666,7 +666,7 @@ function _testrunner_run_testcase( end try - LSP.JSON3.read(testrunnerproc, TestRunnerResult) + LSP.JSON.parse(testrunnerproc, TestRunnerResult) catch err @error "Error from testrunner process" err show_error_message(server, """