Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions jetls-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,66 @@
}
}
},
"plugins": {
"type": "array",
"markdownDescription": "JETLS user plugin configuration. Each entry describes a package to load and activate as a JETLS plugin.\n\nFor example: `[{ \"name\": \"BorrowChecker\", \"uuid\": \"...\" }]`.",
"items": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"markdownDescription": "Package name (e.g. `'BorrowChecker'`)."
},
"uuid": {
"type": "string",
"markdownDescription": "Optional package UUID to disambiguate."
},
"version": {
"type": "string",
"markdownDescription": "Optional version string passed to `Pkg.PackageSpec(version=...)`."
},
"url": {
"type": "string",
"markdownDescription": "Optional git URL passed to `Pkg.PackageSpec(url=...)`."
},
"path": {
"type": "string",
"markdownDescription": "Optional local path passed to `Pkg.PackageSpec(path=...)`. Relative paths are resolved against the workspace root."
},
"subdir": {
"type": "string",
"markdownDescription": "Optional subdirectory within `url`/`path`, passed to `Pkg.PackageSpec(subdir=...)`."
},
"rev": {
"type": "string",
"markdownDescription": "Optional git revision passed to `Pkg.PackageSpec(rev=...)`."
},
"entry": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"markdownDescription": "Optional entrypoint(s) within the package module. If set, JETLS will call these to obtain plugins."
},
"enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Whether this plugin entry is enabled."
}
}
},
"default": []
},
"testrunner": {
"type": "object",
"markdownDescription": "TestRunner integration configuration. See [TestRunner integration](https://aviatesk.github.io/JETLS.jl/release/configuration/#config/testrunner).",
Expand Down
2 changes: 2 additions & 0 deletions src/JETLS.jl
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ include("init-options.jl")
include("config.jl")
include("workspace-configuration.jl")

include("plugins.jl")

include("diagnostic.jl")

include("analysis/Interpreter.jl")
Expand Down
49 changes: 49 additions & 0 deletions src/analysis/Analyzer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,45 @@ CC.typeinf_lattice(::LSAnalyzer) =
CC.ipo_lattice(::LSAnalyzer) =
CC.InferenceLattice(CC.InterMustAliasesLattice(CC.IPOResultLattice.instance))

# Collect `GeneratorErrorReport`s for LSAnalyzer
# ---------------------------------------------
#
# JET's default `JETAnalyzer` reports generator errors by overloading
# `Core.Compiler.InferenceState(..., ::JETAnalyzer)`. JETLS uses `LSAnalyzer` instead,
# so we replicate the same behavior here so that generator errors are surfaced as
# diagnostics in the language server.

"""Internal utility to report generator errors as `JET.GeneratorErrorReport`s."""
function report_generator_error!(analyzer::LSAnalyzer, result::CC.InferenceResult)
mi = result.linfo
m = mi.def::Method
isdefined(m, :generator) || return nothing
CC.may_invoke_generator(mi) || return nothing

world = CC.get_inference_world(analyzer)
try
@ccall jl_code_for_staged(mi::Any, world::UInt)::Any
catch err
report = add_new_report!(analyzer, result, JET.GeneratorErrorReport(mi, err))
JET.stash_report!(analyzer, report)
end
return nothing
end

function CC.InferenceState(
result::CC.InferenceResult,
cache_mode::UInt8,
analyzer::LSAnalyzer,
)
frame = @invoke CC.InferenceState(
result::CC.InferenceResult,
cache_mode::UInt8,
analyzer::ToplevelAbstractAnalyzer,
)
frame === nothing && report_generator_error!(analyzer, result)
return frame
end

# AbstractAnalyzer API
# ====================

Expand Down Expand Up @@ -400,6 +439,16 @@ JETInterface.print_report_message(io::IO, r::BoundsErrorReport) =
inference_error_report_stack_impl(r::BoundsErrorReport) = (length(r.vst)-r.vst_offset):-1:1
inference_error_report_severity_impl(::BoundsErrorReport) = DiagnosticSeverity.Warning

# Prefer a user-visible frame for generator errors.
function inference_error_report_stack_impl(r::JET.GeneratorErrorReport)
for i in 1:length(r.vst)
r.vst[i].file === :none && continue
return i:-1:1
end
return length(r.vst):-1:1
end
inference_error_report_severity_impl(::JET.GeneratorErrorReport) = DiagnosticSeverity.Error

function report_builtin_error!(
analyzer::LSAnalyzer, sv::CC.InferenceState, @nospecialize(f), argtypes::Vector{Any},
@nospecialize(ret), offset::Int
Expand Down
19 changes: 12 additions & 7 deletions src/analysis/Interpreter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export LSInterpreter
using JuliaSyntax: JuliaSyntax as JS
using JET: CC, JET, JuliaInterpreter
using ..JETLS:
AnalysisRequest, AnalysisResult, SavedFileInfo, Server, JETLS, JETLS_DEV_MODE,
AbstractJETLSPlugin, AnalysisRequest, AnalysisResult, SavedFileInfo, Server, JETLS,
JETLS_DEV_MODE, active_plugins,
is_cancelled, send_progress, yield_to_endpoint
using ..JETLS.URIs2
using ..JETLS.LSP
Expand All @@ -21,27 +22,30 @@ Base.getindex(counter::Counter) = counter.count
struct LSInterpreter{S<:Server} <: JET.ConcreteInterpreter
server::S
request::AnalysisRequest
plugins::Vector{AbstractJETLSPlugin}
analyzer::LSAnalyzer
counter::Counter
activation_done::Union{Nothing,Base.Event}
warning_reports::Vector{JETLS.ToplevelWarningReport}
current_node::Base.RefValue{JS.SyntaxNode}
state::JET.InterpretationState
function LSInterpreter(
server::S, request::AnalysisRequest, analyzer::LSAnalyzer, counter::Counter,
server::S, request::AnalysisRequest, plugins::Vector{AbstractJETLSPlugin},
analyzer::LSAnalyzer, counter::Counter,
activation_done::Union{Nothing,Base.Event}
) where S<:Server
return new{S}(server, request, analyzer, counter, activation_done,
return new{S}(server, request, plugins, analyzer, counter, activation_done,
JETLS.ToplevelWarningReport[], Base.RefValue{JS.SyntaxNode}())
end
function LSInterpreter(
server::S, request::AnalysisRequest, analyzer::LSAnalyzer, counter::Counter,
server::S, request::AnalysisRequest, plugins::Vector{AbstractJETLSPlugin},
analyzer::LSAnalyzer, counter::Counter,
activation_done::Union{Nothing,Base.Event},
warning_reports::Vector{JETLS.ToplevelWarningReport},
current_node::Base.RefValue{JS.SyntaxNode},
state::JET.InterpretationState,
) where S<:Server
return new{S}(server, request, analyzer, counter, activation_done,
return new{S}(server, request, plugins, analyzer, counter, activation_done,
warning_reports, current_node, state)
end
end
Expand All @@ -51,14 +55,15 @@ function LSInterpreter(
server::Server, request::AnalysisRequest;
activation_done::Union{Nothing,Base.Event} = nothing
)
return LSInterpreter(server, request, LSAnalyzer(request.entry), Counter(), activation_done)
plugins = active_plugins(server, request.entry)
return LSInterpreter(server, request, plugins, LSAnalyzer(request.entry), Counter(), activation_done)
end

# `JET.ConcreteInterpreter` interface
JET.InterpretationState(interp::LSInterpreter) = interp.state
function JET.ConcreteInterpreter(interp::LSInterpreter, state::JET.InterpretationState)
return LSInterpreter(
interp.server, interp.request, interp.analyzer, interp.counter,
interp.server, interp.request, interp.plugins, interp.analyzer, interp.counter,
interp.activation_done, interp.warning_reports, interp.current_node, state)
end
JET.ToplevelAbstractAnalyzer(interp::LSInterpreter) = interp.analyzer
Expand Down
52 changes: 41 additions & 11 deletions src/analysis/full-analysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -562,15 +562,21 @@ function analyze_parsed_if_exist(
activation_done::Union{Nothing,Base.Event} = nothing
)
uri = entryuri(request.entry)
jetconfigs = getjetconfigs(request.entry)
# Construct the interpreter first so we can apply user plugins to the analysis config.
interp = LSInterpreter(server, request; activation_done)

jetconfigs = copy(getjetconfigs(request.entry))
for plugin in interp.plugins
# Plugins may be loaded dynamically after the server has started and tasks have
# been spawned; use `invokelatest` to avoid world-age issues (Julia 1.12+).
Base.invokelatest(plugin_modify_jetconfigs!, plugin, request.entry, jetconfigs)
end
fi = get_saved_file_info(server.state, uri)
if !isnothing(fi)
filename = uri2filename(uri)
interp = LSInterpreter(server, request; activation_done)
return interp, JET.analyze_and_report_expr!(interp, fi.syntax_node, filename, args...; jetconfigs...)
else
filepath = @something uri2filepath(uri) error(lazy"Unsupported URI: $uri")
interp = LSInterpreter(server, request; activation_done)
return interp, JET.analyze_and_report_file!(interp, filepath, args...; jetconfigs...)
end
end
Expand All @@ -591,7 +597,7 @@ function new_analysis_result(interp::LSInterpreter, request::AnalysisRequest, re
uri2diagnostics = URI2Diagnostics(uri => Diagnostic[] for uri in keys(analyzed_file_infos))
postprocessor = JET.PostProcessor(result.res.actual2virtual)
toplevel_warning_reports_to_diagnostics!(uri2diagnostics, interp.warning_reports, interp.server, postprocessor)
jet_result_to_diagnostics!(uri2diagnostics, result, postprocessor)
jet_result_to_diagnostics!(uri2diagnostics, result, postprocessor, interp.plugins)

(; entry, prev_analysis_result) = request
if !(isempty(result.res.toplevel_error_reports) || isnothing(prev_analysis_result))
Expand Down Expand Up @@ -853,23 +859,47 @@ function analyze_package_with_revise(
uri2diagnostics = URI2Diagnostics(uri => Diagnostic[] for uri in keys(analyzed_file_infos))
postprocessor = JET.PostProcessor()

plugins = active_plugins(server, request.entry)
toplevel_warning_reports_to_diagnostics!(uri2diagnostics, warning_reports, server, postprocessor)
inference_reports = collect_displayable_reports(progress.reports, keys(uri2diagnostics))
inference_reports = collect_displayable_reports(progress.reports, keys(uri2diagnostics), plugins)
unique!(JET.aggregation_policy(analyzer), inference_reports)
jet_inference_error_reports_to_diagnostics!(uri2diagnostics, inference_reports, postprocessor)
jet_inference_error_reports_to_diagnostics!(uri2diagnostics, inference_reports, postprocessor, plugins)
actual2virtual = pkgmod => pkgmod # No virtual module for Revise-based analysis

return AnalysisResult(request.entry, uri2diagnostics, update_analyzer_world(analyzer), analyzed_file_infos, actual2virtual)
end

function collect_displayable_reports(
reports::Vector{JET.InferenceErrorReport}, target_uris
reports::Vector{JET.InferenceErrorReport},
target_uris,
)
return collect_displayable_reports(reports, target_uris, AbstractJETLSPlugin[])
end

function collect_displayable_reports(
reports::Vector{JET.InferenceErrorReport},
target_uris,
plugins::Vector{AbstractJETLSPlugin},
)
filter(reports) do report::JET.InferenceErrorReport
stk = inference_error_report_stack(report)
topframe = report.vst[first(stk)]
uri = @something jet_frame_to_uri(topframe) return false
return uri in target_uris
# 1) Any frame in the report's stack belongs to the current analysis unit.
for i in inference_error_report_stack(report)
frame = report.vst[i]
uri = jet_frame_to_uri(frame)
uri === nothing && continue
uri in target_uris && return true
end

# 2) Plugins can report additional URIs (e.g. errors that carry their own locations).
for plugin in plugins
# Plugins may be loaded dynamically after the server has started and tasks
# have been spawned; use `invokelatest` to avoid world-age issues (Julia 1.12+).
for uri in Base.invokelatest(plugin_additional_report_uris, plugin, report)
uri in target_uris && return true
end
end

return false
end
end

Expand Down
25 changes: 16 additions & 9 deletions src/config.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,23 @@ function merge_and_track(
K = fieldtype(T, key)
old_by_key = Dict{K,T}(getfield(item, key) => item for item in old_config)
new_by_key = Dict{K,T}(getfield(item, key) => item for item in new_config)

# Preserve the ordering of `new_config` (config overlay should control order).
result = T[]
for (k, old_item) in old_by_key
if haskey(new_by_key, k)
push!(result, merge_and_track(on_difference, old_item, new_by_key[k], path))
for new_item in new_config
k = getfield(new_item, key)
if haskey(old_by_key, k)
push!(result, merge_and_track(on_difference, old_by_key[k], new_item, path))
else
push!(result, merge_and_track(on_difference, old_item, nothing, path))
end
end
for (k, new_item) in new_by_key
if !haskey(old_by_key, k)
push!(result, merge_and_track(on_difference, nothing, new_item, path))
end
end
# Keep entries that exist only in `old_config` (append in their original order).
for old_item in old_config
k = getfield(old_item, key)
haskey(new_by_key, k) && continue
push!(result, merge_and_track(on_difference, old_item, nothing, path))
end
return result
end

Expand Down Expand Up @@ -248,8 +252,9 @@ end
mutable struct ConfigChangeTracker
const changed_settings::Vector{ConfigChange}
diagnostic_setting_changed::Bool
plugins_setting_changed::Bool
end
ConfigChangeTracker() = ConfigChangeTracker(ConfigChange[], false)
ConfigChangeTracker() = ConfigChangeTracker(ConfigChange[], false, false)

function (tracker::ConfigChangeTracker)(old_val, new_val, path::Tuple{Vararg{Symbol}})
@nospecialize old_val new_val
Expand All @@ -258,6 +263,8 @@ function (tracker::ConfigChangeTracker)(old_val, new_val, path::Tuple{Vararg{Sym
push!(tracker.changed_settings, ConfigChange(path_str, old_val, new_val))
if !isempty(path) && first(path) === :diagnostic
tracker.diagnostic_setting_changed = true
elseif !isempty(path) && first(path) === :plugins
tracker.plugins_setting_changed = true
end
end
end
Expand Down
Loading