Diagnostic

JETLS reports various diagnostic messages (errors, warnings, hints) to help you catch potential issues in your Julia code. Each diagnostic has a unique code that identifies its category and type.

This document describes all available diagnostic codes, their meanings, default severity levels, and how to configure them to match your project's needs.

Codes

JETLS reports diagnostics using hierarchical, slash-separated codes, following the LSP specification. A code is a slash-separated path, category/.../kind, where:

  • the first segment is a category (syntax, lowering, …), naming the analysis stage that produces the diagnostic;
  • the last segment is the specific kind;
  • optional intermediate segments group related diagnostics.

For example syntax/parse-error, or inference/type-error/keyword when nested. This hierarchy allows fine-grained control over which diagnostics to show and at what severity level through configuration, from a whole category down to an individual code.

All available diagnostic codes are listed below:

Severity levels

Each diagnostic has a severity level that indicates how serious the issue is. JETLS supports four severity levels defined by the LSP specification:

  • Error (1): invalid code that cannot be compiled or loaded (e.g. syntax errors, lowering errors)
  • Warning (2): code that is likely a bug (e.g. undefined variables, type mismatches)
  • Information (3): valid code that is probably unintentional (e.g. unused bindings, unreachable code)
  • Hint (4): stylistic suggestions where the code works as intended but could be written more cleanly (e.g. unsorted import names)

The LSP specification does not prescribe how clients should render each severity level, so the actual display varies by editor. In practice, most editors display Error, Warning, and Information with color-coded underlines (red, yellow, blue) and gutter markers, while Hint is typically rendered with a more subtle indicator such as faded text or an ellipsis (...).[vscode_severity]

You can change the severity of any diagnostic by configuring diagnostic section. Additionally, JETLS supports disabling diagnostics entirely using the special severity value "off" (or 0).

Sources

JETLS uses different diagnostic channels to balance analysis accuracy with response latency. Lightweight checks run as you edit for immediate feedback, while deeper analysis runs when you open or save a file to avoid excessive resource consumption.

Each diagnostic has a source field that identifies which diagnostic channel it comes from. This section explains what each source means, helping you understand when diagnostics update. Additionally, some editors also allow filtering diagnostics by source.

Info

This section contains references to LSP protocol details. You don't need to understand these details to use JETLS effectively - the key takeaway is simply that different diagnostics update at different times (as you edit, when you open or save a file, or when you run tests via TestRunner integration).

JETLS uses three diagnostic sources:

  • JETLS/live: Diagnostics pushed via textDocument/publishDiagnostics as you edit, providing real-time feedback without requiring a file save. Open files are republished whenever their contents change, and unopened files (when diagnostic.all_files is enabled) whenever their diagnostics may have changed, such as after an edit in a related file. Clients whose integration sets the pull_diagnostics initialization option (such as the VSCode extension) receive the diagnostics of open files through textDocument/diagnostic instead. Includes syntax errors and lowering-based analysis (syntax/*, lowering/*).
  • JETLS/save: Diagnostics published by JETLS after full analysis, which runs when you open or save a file (see When full analysis runs), via the push model channel textDocument/publishDiagnostics. These run full analysis including type inference and require loading your code. Includes top-level errors and inference-based analysis (toplevel/*, inference/*).
  • JETLS/extra: Diagnostics from sources other than the analysis of your code: the TestRunner integration (testrunner/*) and the checks of the configuration file (config/*). Published via textDocument/publishDiagnostics.

Analysis stages

JETLS produces diagnostics at different stages of analysis. Each stage owns a code category (the first segment of its codes) and determines what is analyzed, which tool powers it, and through which source its diagnostics are delivered:

CategoryStagePowered bySourceDepends on
syntax/*ParsingJuliaSyntax.jlJETLS/live—
lowering/*LoweringJuliaLowering.jlJETLS/liveparsing, code loading[code_loading_dependency]
toplevel/*Code loadingJuliaInterpreter.jl, LoweredCodeUtils.jlJETLS/saveparsing
inference/*Type inferenceJET.jlJETLS/savecode loading
config/*ConfigurationJETLSJETLS/extra—
testrunner/*Test executionTestRunner.jlJETLS/extramanual run

The first four stages belong to two cooperating layers, not a single linear pipeline. Parsing and lowering make up live analysis, and code loading and type inference make up full analysis. Code-loading errors limit type inference and may leave live analysis without a complete module context; context-independent live checks remain available. See Analysis for how the layers work together.

Test execution is separate and runs only when you trigger it. Configuration checks are separate as well and only concern the configuration file.

Parsing and lowering (the live stages) report byte-precise source ranges, and configuration checks report at the keys they concern. Code loading, type inference, and test execution report at line granularity.

Parsing (syntax/*)

JuliaSyntax.jl parses your source text into syntax trees. It runs on every edit without executing your code, so syntax/* diagnostics are delivered through the JETLS/live source and update as you type. Because every later stage builds on the parse result, unparsable code yields only syntax/* diagnostics. Syntax warnings, by contrast, do not prevent the later stages from analyzing the code. See syntax/* for the diagnostic codes.

Lowering (lowering/*)

JuliaLowering.jl lowers the parsed syntax tree into the intermediate representation JETLS analyzes. lowering/* diagnostics are delivered through the JETLS/live source and update as you edit. Lowering relies on binding and scope information, not types, so it flags undefined or unused variables, unreachable code, and the like — but not type-level problems, which require type inference. Macro expansion can execute macro code. Most checks are self-contained, but macro expansion and global-name resolution need a module context, so they additionally depend on code loading[code_loading_dependency]. See lowering/* for the diagnostic codes.

Code loading (toplevel/*)

JuliaInterpreter.jl and LoweredCodeUtils.jl load your code as the first step of full analysis. toplevel/* diagnostics are delivered through the JETLS/save source. Loading stops at the first top-level error, and code after it is not analyzed. See toplevel/* for the diagnostic codes.

Type inference (inference/*)

JET.jl runs type inference over your loaded code as the second step of full analysis. inference/* diagnostics are delivered through the JETLS/save source. Inference only covers code that loaded successfully: after a toplevel/error (e.g. a missing dependency), JETLS skips inference of method bodies entirely and reports inference/* diagnostics only for top-level code before the error. See inference/* for the diagnostic codes.

Configuration (config/*)

JETLS checks the .JETLSConfig.toml file at the workspace root, reporting issues through the JETLS/extra source at the keys they concern. They are reported when the server starts, update as you edit the file while it is open in the editor, and when the file changes on disk otherwise. Not all clients synchronize TOML files with JETLS, though. With such a client, JETLS sees the file as unopened, so its diagnostics update only when the file changes on disk, such as when you save it. Whenever JETLS sees the file as unopened, its diagnostics are reported only if diagnostic.all_files is enabled. See config/* for the diagnostic codes.

Test execution (testrunner/*)

TestRunner.jl runs your tests via the TestRunner integration, reporting failures through the JETLS/extra source. Unlike the other categories, these run only when you trigger them manually, not automatically on edit or save. See testrunner/* for the diagnostic codes.

Reference

This section provides detailed explanations for each diagnostic code. For every diagnostic, you'll find:

  • A description of what the diagnostic detects
  • Its default severity level and source
  • Code examples demonstrating when the diagnostic is reported
  • Example diagnostic messages (shown in code comments)

Here is a summary table of the diagnostics explained in this section:

CodeDefault SeveritySourceDescription
syntax/parse-errorErrorJETLS/liveSyntax parsing errors detected by JuliaSyntax.jl
syntax/parse-warningWarningJETLS/liveSyntax warnings detected by JuliaSyntax.jl
lowering/errorErrorJETLS/liveGeneral lowering errors
lowering/macro-expansion-errorError/WarningJETLS/liveIssues detected during macro expansion
lowering/undef-global-varWarningJETLS/liveReferences to undefined global variables
lowering/undef-local-varWarning/InformationJETLS/liveReferences to undefined local variables
lowering/ambiguous-soft-scopeWarningJETLS/liveAssignment in soft scope shadows a global variable
lowering/captured-boxed-variableInformationJETLS/liveVariables captured by closures that require boxing
lowering/unconstrained-static-parameterWarningJETLS/liveStatic parameters not used in function parameter types
lowering/orphaned-docstringInformationJETLS/liveDocstrings not attached to any definition
lowering/unused-argumentInformationJETLS/liveFunction arguments that are never used
lowering/unused-localInformationJETLS/liveLocal variables that are never used
lowering/unused-assignmentInformationJETLS/liveAssignments whose values are never read
lowering/unused-importInformationJETLS/liveImported names that are never used
lowering/unreachable-codeInformationJETLS/liveCode after a block terminator that is never reached
lowering/inactive-codeHintJETLS/liveCode excluded by @static in the current environment
lowering/unsorted-import-namesHintJETLS/liveImport/export names not sorted alphabetically
toplevel/errorErrorJETLS/saveErrors during code loading
toplevel/missing-concretizationErrorJETLS/saveTop-level code needs a non-concretized binding value
toplevel/concretization-timeoutErrorJETLS/saveConcrete execution of top-level code exceeded its time limit
toplevel/method-overwriteWarningJETLS/saveMethod definitions that overwrite previous ones
toplevel/abstract-fieldHintJETLS/saveStruct fields with abstract types
toplevel/unsupported-featureWarningJETLS/saveUnsupported code analyzed with an approximation
toplevel/analysis-skippedWarning/InformationJETLS/saveFiles skipped by full analysis
toplevel/analysis-stoppedWarningJETLS/saveAnalysis units stopped at an excluded file
inference/undef-global-varWarningJETLS/saveReferences to undefined global variables
inference/field-errorWarningJETLS/saveAccess to non-existent struct fields
inference/bounds-errorWarningJETLS/saveOut-of-bounds field access by index
inference/method-errorWarningJETLS/saveNo matching method found for function calls
inference/undef-keywordWarningJETLS/saveMissing required keyword argument
inference/type-error/keywordWarningJETLS/saveKeyword argument value type mismatch
inference/type-error/type-assertWarningJETLS/saveType assertion failures
inference/type-error/non-bool-condWarningJETLS/saveNon-boolean value used in boolean context
inference/failing-testWarningJETLS/save@test that always fails
testrunner/test-failureErrorJETLS/extraTest failures from TestRunner integration
config/deprecated-keyWarningJETLS/extraDeprecated keys in .JETLSConfig.toml
config/deprecated-valueWarningJETLS/extraDeprecated values in .JETLSConfig.toml

Syntax diagnostic (syntax/*)

Syntax diagnostics come from JuliaSyntax.jl's parser and are reported with byte-precise ranges (see Parsing).

Syntax parse error (syntax/parse-error)

Default severity: Error

Syntax parsing errors detected by JuliaSyntax.jl. These indicate invalid Julia syntax that prevents the code from being parsed.

Example:

function parse_error(x)
    println(x  # Expected `)` or `,` (JETLS syntax/parse-error)
end

Syntax parse warning (syntax/parse-warning)

Default severity: Warning

Syntax warnings detected by JuliaSyntax.jl. These indicate code that parses but is likely unintended or written in a discouraged form, such as redundant parentheses or quoting. Unlike syntax/parse-error, they do not prevent the code from being analyzed further.

Example:

import Base: (sin)  # parentheses are not required here (JETLS syntax/parse-warning)

Lowering diagnostic (lowering/*)

Lowering diagnostics come from JETLS's lowering-phase analysis and are reported with byte-precise ranges (see Lowering).

Lowering error (lowering/error)

Default severity: Error

General lowering errors that don't fit into more specific categories.

Examples:

macro lowering_error(x)
    $(x)  # `$` expression outside string or quote block (JETLS lowering/error)
end

function unresolved_goto()
    @label retry
    inner = () -> @goto retry  # label `retry` referenced but not defined (JETLS lowering/error)
    inner()                    # (`@goto` cannot cross function boundaries)
end

Macro expansion error (lowering/macro-expansion-error)

Default severity: Error or Warning (see below)

Issues detected during macro expansion. The macro name being unresolved or a macro body throwing surfaces as Error severity:

function macro_expand_error()
    @undefined_macro ex  # Macro name `@undefined_macro` not found (JETLS lowering/macro-expansion-error)
end
macro myinline(ex)
    Meta.isexpr(ex, :function) || error("Expected long function definition")
    return :(@inline $ex)
end
@myinline callsin(x) = sin(x)  # Error expanding macro
                               # Expected long function definition (JETLS lowering/macro-expansion-error)

For a subset of Base macros (@spawn, @test, @info, @testset, ...), JETLS provides new-style stubs that validate arguments at expansion time and report problems via a side channel without aborting expansion, so downstream analyses (undef-var, references, hover, signature help, ...) keep running on the rest of the macrocall. The severity mirrors what Base would do — Error for cases Base rejects, Warning for cases Base accepts silently or only deprecates:

# Error: Base also rejects.
Threads.@spawn :badname body   # unsupported threadpool in @spawn: badname
                               # (JETLS lowering/macro-expansion-error)

@info "msg" k=1 k=2            # @info: keyword `k` provided more than once
                               # (JETLS lowering/macro-expansion-error)

@test x skip=true broken=true  # invalid test macro call: cannot set both
                               # `skip` and `broken` keywords
                               # (JETLS lowering/macro-expansion-error)

# Warning: Base accepts silently or only deprecates.
@testset "a" "b" begin end     # Multiple descriptions provided to @testset.
                               # This is deprecated and may error in the future.
                               # (JETLS lowering/macro-expansion-error)

@testset verbose=true verbose=false begin end
                               # @testset: option `verbose` provided more than once
                               # (JETLS lowering/macro-expansion-error)

Undefined global variable (lowering/undef-global-var)

Default severity: Warning

References to undefined global variables, detected during lowering analysis. This diagnostic provides immediate feedback as you type.

Example:

function undef_global_var()
    ret = sin(undefined_var)  # `Main.undefined_var` is not defined (JETLS lowering/undef-global-var)
    return ret
end

This diagnostic detects simple undefined global variable references. For more comprehensive detection (including qualified references like Base.undefvar), see inference/undef-global-var (source: JETLS/save).

Undefined local variable (lowering/undef-local-var)

Default severity: Warning or Information

References to local variables that may be used before being defined. This diagnostic provides immediate feedback based on CFG-aware analysis on lowered code.

The severity depends on the certainty of the undefined usage:

  • Warning: The variable is definitely used before any assignment (strict undef - guaranteed UndefVarError at runtime)
  • Information: The variable may be undefined depending on control flow (e.g., assigned only in one branch of an if statement)

Examples:

function strict_undef()
    println(x)  # Variable `x` is used before it is defined (JETLS lowering/undef-local-var)
                # Severity: Warning (strict undef)
    x = 1       # RelatedInformation: `x` is defined here
    return x
end

function maybe_undef(cond)
    if cond
        y = 1   # RelatedInformation: `y` is defined here
    end
    return y  # Variable `y` may be used before it is defined (JETLS lowering/undef-local-var)
              # Severity: Information (maybe undef)
end

The diagnostic is reported at the first use location, with relatedInformation pointing to definition sites to help understand the control flow.

Workaround: Using `@isdefined` guard

When a variable is conditionally assigned, you can rewrite the program logic using @isdefined so that the compiler can track the definedness:

function guarded(cond)
    if cond
        y = 42
    end
    if @isdefined(y)
        return sin(y)  # No diagnostic: compiler knows `y` is defined here
    end
end
Correlated condition analysis

The analysis recognizes correlated conditions: if a variable is assigned under a condition and later used under the same condition, no diagnostic is emitted. This works with simple variables, && chains, and nested if blocks:

function correlated(cond)
    if cond
        y = 42
    end
    if cond
        return sin(y)  # No diagnostic: analysis tracks that `cond`
                       # is the same in both branches
    end
end

This is limited to conditions that are simple local variables or && chains of local variables (e.g. if x && z). Compound expressions like if x > 0 are not tracked as correlated conditions.

Workaround: Using `@assert @isdefined` as a hint

There are cases where you know a variable is always defined at a certain point, but the analysis cannot prove it. This includes compound conditions (e.g. if !isnothing(x)), complex control flow, or general runtime invariants that the compiler cannot figure out statically. In such cases, you can use @assert @isdefined(var) "..." as a hint:

function compound_condition(x)
    if !isnothing(x)
        y = sin(x)
    end
    if !isnothing(x)
        @assert @isdefined(y) "compiler hint"
        return cos(y)  # No diagnostic after the assertion
    end
end

This hint allows the compiler to avoid generating unnecessary UndefVarError handling code, and also serves as documentation that you've verified the variable is defined at this point.

Noreturn functions as guards

Calls to throw, error, rethrow, and exit are recognized as noreturn. If an else branch calls one of these, the analysis knows code after the branch can only be reached when the variable is defined:

function guarded(x)
    if x > 0
        y = x
    else
        error("x must be positive")
    end
    return sin(y)  # No diagnostic: error() guarantees y is defined
end

See the caveat on noreturn functions in the unreachable code section for limitations.

Ambiguous soft scope (lowering/ambiguous-soft-scope)

Default severity: Warning

Reported when a variable is assigned inside a for, while, or try/catch block at the top level of a file, and a global variable with the same name already exists[on_soft_scope]. This assignment is ambiguous because it behaves differently depending on where the code runs:

  • In the REPL or notebooks: assigns to the existing global
  • In a file: creates a new local variable, leaving the global unchanged

Example (A Common Confusion adapted from the Julia manual):

ambiguous-scope.jl

# Print the numbers 1 through 5
global i = 0
while i < 5
    i += 1  # Assignment to `i` in soft scope is ambiguous (JETLS lowering/ambiguous-soft-scope)
            # Variable `i` may be used before it is defined (JETLS lowering/undef-local-var)
    println(i)
end

This diagnostic matches the warning that Julia itself emits at runtime. Running the example above as a file produces:

julia ambiguous-scope.jl

┌ Warning: Assignment to `i` in soft scope is ambiguous because a global variable by the same name exists: `i` will be treated as a new local. Disambiguate by using `local i` to suppress this warning or `global i` to assign to the existing global variable.
└ @ ambiguous-scope.jl:4
ERROR: LoadError: UndefVarError: `i` not defined in local scope
Suggestion: check for an assignment to a local variable that shadows a global of the same name.
Stacktrace:
...
Why is `lowering/undef-local-var` also reported?

Since i += 1 desugars to i = i + 1, the new local i is read before being assigned, which also triggers lowering/undef-local-var and causes the UndefVarError shown above at runtime.

Code actions available

Two quick fixes are offered: "Insert global i declaration" (preferred) to assign to the existing global, and "Insert local i declaration" to explicitly mark the variable as local and suppress the warning.

Notebook mode

This diagnostic is suppressed for notebooks, where soft scope semantics are enabled (matching REPL behavior).

Captured boxed variable (lowering/captured-boxed-variable)

Default severity: Information

Reported when a variable is captured by a closure and requires "boxing" due to being assigned multiple times. Captured boxed variables are stored in heap-allocated containers (a.k.a. Core.Box), which can cause type instability and hinder compiler optimizations.[performance_tip]

Example:

function captured_variable()
    x = 1           # `x` is captured and boxed (JETLS lowering/captured-boxed-variable)
    f = () ->
        println(x)  # RelatedInformation: Closure at L3:9 captures `x`
    x = 2           # (`x` is reassigned after capture)
    return f
end

The diagnostic includes relatedInformation showing where the variable is captured:

function multi_capture()
    x = 1               # `x` is captured and boxed (JETLS lowering/captured-boxed-variable)
    f = () ->
        println(x)      # RelatedInformation: Closure at L3:9 captures `x`
    g = () ->
        println(x + 1)  # RelatedInformation: Closure at L5:9 captures `x`
    x = 2
    return f, g
end

Variables captured by closures but assigned only once before closure definition do not require boxing and are not reported:

function not_boxed()
    x = 1
    f = () -> x  # OK: `x` is only assigned once
    return f
end
Workaround

When you need to capture a variable that changes, consider using a let block:

function with_let()
    x = 1
    f = let x = x
        () -> x  # Captures the value of `x` at this point
    end
    x = 2
    return f()  # Returns 1, not 2
end

or mutable container like Ref to avoid direct assignment to the captured variable:

function with_mut()
    x = Ref(1)
    f = () -> x[]
    x[] = 2
    return f()
end
Box optimization difference from the flisp lowerer

The generation of captured boxes is an implementation detail of the code lowerer (JuliaLowering.jl) used internally by JETLS, and the conditions under which captured boxes are created may change in the future. The control flow dominance analysis used for captured variable detection in the current JuliaLowering.jl is quite primitive, so captured boxes may occur even when programmers don't expect them. Also note that the cases where the flisp lowerer (a.k.a. code_lowered) generates Core.Box do not necessarily match the cases where JETLS reports captured boxes.

Unconstrained static parameter (lowering/unconstrained-static-parameter)

Default severity: Warning

Static parameters that are declared by a method but do not appear in the type of any function parameter. Such a type variable cannot be deduced when the method is called and may be undefined when the method body or return type annotation refers to it. This mirrors the warning Julia itself prints when evaluating such a method definition.

Example:

f(::T) where {T,S} = S  # Method definition declares type variable `S` but does not use it in the type of any function parameter
                        # (JETLS lowering/unconstrained-static-parameter)

This diagnostic is about whether the type variable can be deduced at the call site, not whether the static parameter is referenced in the method body. A static parameter that appears in a function parameter type is not reported, even if the body does not use it:

f(::T) where T = nothing  # OK: `T` appears in a function parameter type

Bounds on constrained type variables are also considered, but a bound reference only constrains type variables declared earlier in the where clause:

f(::T) where {S,T<:S} = S  # OK: `S` constrains the dispatched type variable `T`
f(::T) where {T<:S,S} = S  # Method definition declares type variable `S` but does not use it in the type of any function parameter
                           # (JETLS lowering/unconstrained-static-parameter)

Orphaned docstring (lowering/orphaned-docstring)

Default severity: Information

Reported on string literals that look like docstrings but are not attached to any definition, so they silently document nothing. Julia attaches a string literal as a docstring only when:

  • it appears at the top level, in a module body, in a begin block, or in a struct body (as a field docstring)
  • the documented expression starts on the next line, without blank lines or comments in between
  • it is a plain string literal, not a string macro such as raw"..."

Elsewhere, docstrings must be written with @doc, and docstrings in local scope (e.g. in function bodies) are never attached.

Examples:

"""
    foo()
"""  # Docstring is not attached to any definition: the documented expression must start on the next line
     # (JETLS lowering/orphaned-docstring)
foo() = nothing

@static if Sys.iswindows()
    """
        bar()
    """              # Docstring is not attached to any definition: docstrings in this block are recognized only with `@doc`
    bar() = nothing  # (JETLS lowering/orphaned-docstring)
end

raw"""
    baz(path)
"""                  # Docstring is not attached to any definition: `raw` strings are recognized as docstrings only with `@doc`
baz(path) = nothing  # (JETLS lowering/orphaned-docstring)

function qux()
    "Python-style docstring"  # Docstring is not attached to any definition: docstrings are not recognized in local scope
    nothing                   # (JETLS lowering/orphaned-docstring)
end

Field docstrings are also reported when the struct itself has no docstring, since Julia then discards them[field_docstrings]. The diagnostic is reported on the struct name, with the discarded field docstrings listed as related information:

struct Point            # Field docstrings of `Point` are discarded because `Point` has no docstring
    "The x coordinate"  # (JETLS lowering/orphaned-docstring)
    x::Float64
    "The y coordinate"
    y::Float64
end

Documenting the struct makes the field docstrings available. Adding a docstring later with @doc "..." Point does not, since Julia collects field docstrings only from the documented struct definition. Structs wrapped by macros other than @kwdef are not checked, since such macros may handle field docstrings themselves.

Strings whose value is used, such as the last expression of a function body, are not reported. Neither are strings in begin blocks passed to macros, since macros may give them a meaning (e.g. argument names in ArgParse.@add_arg_table!), nor string macro calls other than raw"...", which may have side effects (e.g. py"...").

Code action available

The "Attach docstring to the following definition" code action prefixes the docstring with @doc and removes the blank lines in between as needed. It is not offered when comments separate the docstring from the next expression, since the intended target is then unclear, nor for a string at the start of a file or module, which usually describes the whole file.

Notebook mode

Top-level strings are not reported in notebooks, where the last expression of a cell is displayed.

Unused argument (lowering/unused-argument)

Default severity: Information

Function arguments that are declared but never used in the function body. The argument is marked with the Unnecessary tag.[unnecessary_tag]

By default, arguments with names starting with _ are not reported; see allow_unused_underscore.

Example:

function unused_argument(x, y)  # Unused argument `y` (JETLS lowering/unused-argument)
    return x + 1
end
Code action available

You can use the "Prefix with '_'" code action to quickly rename unused arguments, indicating they are intentionally unused.

Unused local variable (lowering/unused-local)

Default severity: Information

Local variables that are never used anywhere in their scope. The variable is marked with the Unnecessary tag.[unnecessary_tag]

By default, variables with names starting with _ are not reported; see allow_unused_underscore.

Example:

function unused_local()
    x = 10  # Unused local binding `x` (JETLS lowering/unused-local)
    return println(10)
end

When an unused local is introduced by an assignment expression that is returned from tail position, the diagnostic explains that the local binding is not read. For example, the function below returns the assignment expression's value by Julia's implicit return rule, but the binding s itself is not read after the assignment:

function wrapped(x)
    s = "($x)"  # consider `return s` to return the binding explicitly
end
Code action available

Several code actions are available for this diagnostic:

  • "Prefix with '_'" to indicate the variable is intentionally unused
  • "Insert explicit return" for simple tail assignments where the assigned binding should be returned explicitly
  • "Delete assignment" to remove only the left-hand side (keeping the right-hand side expression)
  • "Delete statement" to remove the entire assignment statement

Unused assignment (lowering/unused-assignment)

Default severity: Information

Assignments to local variables whose values are never read. This diagnostic targets individual assignments where the value is overwritten or the function exits before the value is read. The assignment is marked with the Unnecessary tag.[unnecessary_tag]

This diagnostic does not overlap with lowering/unused-local: unused-local reports variables that are never used anywhere, while unused-assignment reports specific assignments to variables that are used elsewhere. For example:

function f(x::Bool)
    if x
        z = "Hi"
        println(z)  # z is used here, so `lowering/unused-local` is NOT reported
    end
    if x
        z = "Hey"   # but this assignment's value is never read → `lowering/lunused-assignment`
    end
end

Compare with a fully unused variable, which only triggers unused-local:

function g()
    y = 42  # y is never used anywhere → `lowering/unused-local`
end

When the unused assignment expression is returned from tail position, the diagnostic explains that the value is returned implicitly. For example, the final assignment below returns the assignment expression's value, but the binding s itself is not read after that assignment:

function build_string(xs)
    s = "("
    for x in xs
        s *= x
    end
    s *= ")"  # consider `return s` to return the binding explicitly
end
Code action available

Several code actions are available for this diagnostic:

  • "Insert explicit return" for simple tail assignments where the assigned binding should be returned explicitly
  • "Delete assignment" to remove only the left-hand side (keeping the right-hand side expression)
  • "Delete statement" to remove the entire assignment statement
Closure-captured variables

Variables captured by closures are excluded from this analysis to avoid false positives, since the CFG cannot precisely model when closures are called.

Unused import (lowering/unused-import)

Default severity: Information

Reported when an explicitly imported name is never used within the same module space. This diagnostic helps identify unnecessary imports that can be removed to keep your code clean. The unused name is marked with the Unnecessary tag.[unnecessary_tag]

Example:

using Base: sin, cos  # Unused import `cos` (JETLS lowering/unused-import)

examplefunc() = sin(1.0)  # Only `sin` is used

The diagnostic is reported for explicit imports (using M: name or import M: name), not for bulk imports like using M which bring in all exported names.

This diagnostic scans all files within the module space to detect usages, so an import is only reported as unused if the name is not used anywhere in your module.

Code action available

Use the "Remove unused import" code action to delete the unused name. If it's the only name in the statement, the entire statement is removed.

Limitation

Usages introduced only through macro expansion cannot be detected. For example, in the following code, sin appears unused even though it is used inside the macro-generated code:

using Base: sin  # Incorrectly reported as unused

macro gensincall(x)
    :(sin($(esc(x))))
end
@gensincall 42

Workarounds include using the binding directly in the macro body:

macro gensincall(x)
    f = sin  # `sin` is used here
    :($f($(esc(x))))
end

or passing the binding as part of the macro argument:

macro gencall(ex)
    :($(esc(ex)))
end
@gencall sin(42)  # `sin` is used here

Unused label (lowering/unused-label)

Default severity: Information

Reported when a @label is declared but never referenced by any @goto in the same function body. The label is marked with the Unnecessary tag.[unnecessary_tag]

Example:

function unused_label()
    @label spare  # Unused label `spare` (JETLS lowering/unused-label)
    return 1
end

Because @goto cannot cross function boundaries, a @label in an outer function is also unused even when an inner closure references the same name:

function outer()
    @label here  # Unused label `here` (JETLS lowering/unused-label)
    inner = () -> @goto here  # also reported as `lowering/error`
    inner()
end
Code action available

Use the "Remove unused label" code action to delete the @label statement.

Unreachable code (lowering/unreachable-code)

Default severity: Information

Reported when code appears after a statement that always exits the current block, making subsequent code unreachable. The unreachable code is marked with the Unnecessary tag.[unnecessary_tag]

Example:

function after_return()
    return 1
    x = 2  # Unreachable code (JETLS lowering/unreachable-code)
    y = 3  # Also unreachable
end

function all_branches_return(x)
    if x > 0
        return 1
    else
        return -1
    end
    println("unreachable")  # Unreachable code (JETLS lowering/unreachable-code)
end

function after_continue()
    for i = 1:10
        continue
        println(i)  # Unreachable code (JETLS lowering/unreachable-code)
    end
end

function after_throw()
    throw(ErrorException("error"))
    cleanup()  # Unreachable code (JETLS lowering/unreachable-code)
end

function after_error()
    error("something went wrong")
    cleanup()  # Unreachable code (JETLS lowering/unreachable-code)
end

function after_rethrow()
    try
        do_something()
    catch
        rethrow()
        println("unreachable")  # Unreachable code (JETLS lowering/unreachable-code)
    end
end
Code action available

A "Delete unreachable code" quick fix is available that removes the unreachable region along with surrounding whitespace, from the end of the terminating statement to the end of the dead code.

Caveat on noreturn functions

The analysis assumes that error, rethrow, and exit never return. If any loaded code adds an overload that returns normally, the analysis may produce incorrect results. Such overloading is extremely unlikely in practice, and this possibility is accepted as a trade-off for better diagnostics.

Inactive code (lowering/inactive-code)

Default severity: Hint

Reported on @static branches that are not taken in the current environment. JETLS evaluates @static conditions during analysis, so only the branch taken on the analyzing machine (its OS, Julia version, etc.) is analyzed; the other branches are excluded entirely, meaning LSP features such as diagnostics, hover, and inlay hints are unavailable inside them. The inactive region is marked with the Unnecessary tag,[unnecessary_tag] so editors render it grayed out.

Example (analyzed on a non-Windows machine):

function home_dir()
    @static if Sys.iswindows()
        ENV["USERPROFILE"]  # Inactive `@static` branch (condition evaluated to `false`)
                            # (JETLS lowering/inactive-code)
    else
        ENV["HOME"]
    end
end

Unlike lowering/unreachable-code, this does not indicate a problem: the branch is intentionally inactive here and may be active in other environments, so no removal action is offered.

Unsorted import names (lowering/unsorted-import-names)

Default severity: Hint

Reported when names in import, using, export, or public statements are not sorted alphabetically. This is a style diagnostic that helps maintain consistent ordering of imports and exports.

Expected sort order:

  • Case-sensitive comparison (A < Z < a < z)
  • For as expressions like using Foo: bar as baz, sorted by original name (bar), not the alias
  • Relative imports: dots are included in the sort key (..Base < Base < Core)

Example:

import Foo: c, a, b  # Names are not sorted alphabetically (JETLS lowering/unsorted-import-names)

export bar, @foo  # Names are not sorted alphabetically (JETLS lowering/unsorted-import-names)
Code action available

The "Sort import names" code action automatically fixes the ordering. When the sorted result exceeds 92 characters ( Julia's conventional maximum line length), the code action wraps to multiple lines with 4-space continuation indent.

Top-level diagnostic (toplevel/*)

Top-level diagnostics are reported by full analysis about how it loads your code, at line granularity (see Code loading).

Top-level error (toplevel/error)

Default severity: Error

A general wrapper for errors that occur while JETLS loads and evaluates top-level code for analysis. This code identifies the phase in which the error occurred—top-level code loading—rather than a specific kind of error. The diagnostic message describes the underlying error.

Common examples include:

  • Package-loading failures caused by missing, uninstantiated, or unresolved dependencies (the most frequent cause)
  • Type definition failures
  • References to undefined names at the top level
  • Code that cannot be lowered or whose macros fail to expand
  • Other errors during module evaluation

Examples:

struct ToplevelError  # JET could not execute this top-level code: UndefVarError: `Unexisting` not defined in `JETLS`
                      #
                      # Suggestion: check for spelling errors or missing imports. (JETLS toplevel/error)
    x::Unexisting
end

using UnexistingPkg  # Package JETLS does not have UnexistingPkg in its dependencies.
                     # [...]
                     # (JETLS toplevel/error)

JETLS stops loading your code as soon as it encounters a top-level error, so code after the error is not analyzed, and Inference diagnostic for method bodies will not be available until the error is resolved. Fixing the error may reveal another one later in the code.

Check the package environment

A common case is an error indicating that a specific dependency cannot be loaded from the analyzed package's environment. This can happen when the environment has not been instantiated, or when Project.toml has changed but Manifest.toml has not been resolved. Run Pkg.instantiate() for an uninstantiated environment, or Pkg.resolve() when the manifest is out of date. Finally, verify that the package can be loaded successfully in a Julia REPL.

Missing concretization (toplevel/missing-concretization)

Default severity: Error

A specialized top-level error reported when JET needs the actual value of a top-level binding while loading code for analysis, but the binding was not concretized. This often happens when a global binding is used to define a type or method and JET cannot determine the binding's concrete value during analysis. As with toplevel/error, JETLS does not analyze the statement that needs the value or any code after it. This diagnostic is specific to script analysis. Package analysis uses the catch-all concretization pattern :(x_) and evaluates all top-level code.

For example, suppose scripts/random-type.jl contains:

RandomType = rand((Bool, Int))

struct Container
    value::RandomType  # `Main.RandomType` must have a concrete value for JETLS top-level analysis.
                       # [...]
                       # (JETLS toplevel/missing-concretization)
end

JETLS needs the actual value of RandomType to analyze the definition of Container. Adding const can fix this diagnostic in some cases. However, JETLS can treat the value of a const binding as concrete only when its right-hand side can be inferred as a single concrete value. In this example, JETLS cannot infer a single concrete result for rand((Bool, Int)), so use the full_analysis.concretization_patterns configuration to allow JETLS to evaluate the assignment during full analysis.

Code action available

Use the "Create .JETLSConfig.toml concretization pattern for RandomType" code action. If the configuration file already exists, the action starts with "Add" instead. It creates or updates the configuration with this entry:

[[full_analysis.concretization_patterns]]
pattern = "RandomType = x_"
path = "scripts/random-type.jl"

The generated path identifies the source file containing the assignment. This may differ from the file containing the diagnostic when the binding is defined in an included file. Scoping the pattern this way prevents it from affecting unrelated files. Here, x_ matches any right-hand-side expression. See full_analysis.concretization_patterns for details, including how patterns are matched and executed.

`UndefVarError` can be caused by missing concretization

toplevel/missing-concretization is reported when JET can track a required binding and determine that its concrete value is unavailable. An unevaluated top-level call may instead create or import bindings indirectly, for example through @eval. If JET cannot track those effects back to the call, it may only observe that the binding is undefined and report toplevel/error with UndefVarError.

For example, suppose scripts/load-types.jl contains:

const type_name = :SomeType
load_types() = @eval using SomeModule: $type_name
load_types()

struct Container
    value::SomeType  # JET could not execute this top-level code: UndefVarError: `SomeType` not defined in `Main`
                     # [...]
                     # (JETLS toplevel/error)
end

Normal Julia execution calls load_types() and imports SomeType. During script analysis, however, the call is not evaluated unless selected for concretization. Because the binding would be imported indirectly through @eval, JET cannot associate the undefined SomeType with the unevaluated call and reports the generic toplevel/error instead.

In this case, manually add the pattern to .JETLSConfig.toml:

[[full_analysis.concretization_patterns]]
pattern = "load_types()"
path = "scripts/load-types.jl"

Concretization timeout (toplevel/concretization-timeout)

Default severity: Error

Reported when concretely executing a single top-level statement exceeds the configured time limit (10 seconds by default). JETLS stops execution there and, as with toplevel/error, does not analyze that statement or any code after it. The diagnostic includes a stack trace when available. This is distinct from toplevel/missing-concretization: the code was being executed, rather than a required binding value being unavailable.

JETLS executes top-level code when needed to load method or type definitions, handle @eval, or perform in-place updates of concretized values. Configured concretization patterns also select code for execution, and package analysis executes all top-level code. During script analysis, JETLS interprets the executed code and the functions it calls using JuliaInterpreter.jl, so even code that finishes quickly when run normally can exceed the limit.

For example, suppose scripts/word-lookup.jl contains:

using Downloads

const WORDS_URL =
    "https://raw.githubusercontent.com/dwyl/english-words/master/words_alpha.txt"

let # JET stopped the concrete execution of this top-level statement after 10.0 seconds (`concretization_timeout`).
    # [...]
    # Stacktrace:
    #   [...]
    #  [18] readlines(filename::String)
    #    @ Base io.jl:727
    # (JETLS toplevel/concretization-timeout)
    path = Downloads.download(WORDS_URL; timeout=5.0)
    words = Set(readlines(path))
    global is_known_word(word::AbstractString) = word in words
end

is_known_word captures the local words, so JETLS must execute the whole let block to load the definition. The download completes within the limit, but the stack trace shows that the time went into readlines: the word list has hundreds of thousands of lines, and reading them in the interpreter takes longer than 10 seconds.

To run the calls in this block natively, add a full_analysis.concretization_patterns entry matching the let block to .JETLSConfig.toml in the workspace root:

[[full_analysis.concretization_patterns]]
pattern = """
let
    path = Downloads.download(WORDS_URL; timeout=timeout_)
    body__
end
"""
path = "scripts/word-lookup.jl"

timeout_ matches any timeout value and body__ matches the remaining statements, so the pattern keeps matching when those change. With this entry, JETLS executes the entire let block, including the download, and cannot interrupt its calls. Keep network operations bounded in the program itself, as the timeout keyword does here. Package source analysis already runs calls natively and needs no such pattern.

If the code is slow even when run natively, raise full_analysis.concretization_timeout. See its configuration reference for timing rules and limitations; disabling it with "inf" risks hanging analysis indefinitely.

If the code does not terminate, for example a loop that defines methods with @eval on every iteration, move the definitions out of the loop so that JETLS analyzes the loop instead of executing it.

Method overwrite (toplevel/method-overwrite)

Default severity: Warning

Reported when a method with the same signature is defined multiple times within a package. This typically indicates an unintentional redefinition that overwrites the previous method.

Example:

function duplicate(x::Int)
    return x + 1
end

function duplicate(x::Int, y::Int=2)  # Method definition duplicate(x::Int) in module MyPkg overwritten
                                      # (JETLS toplevel/method-overwrite)
    return x + y
end

The diagnostic includes a link to the original definition location via relatedInformation, making it easy to navigate to the first definition.

Abstract field type (toplevel/abstract-field)

Default severity: Hint

Reported when a struct field has an abstract type, which can cause performance issues due to type instability. Storing values in abstractly-typed fields often prevents the compiler from generating optimized code. Since abstract fields are often intentional, this diagnostic is reported as a hint by default; raise its severity with pattern-based configuration to make it more prominent.

Example:

struct MyStruct
    xs::Vector{Integer}  # `MyStruct` has abstract field `xs::Vector{Integer}`
                         # (JETLS toplevel/abstract-field)
end

struct AnotherStruct
    data::AbstractVector{Int}  # `AnotherStruct` has abstract field `data::AbstractVector{Int}`
                               # (JETLS toplevel/abstract-field)
end

To fix this, use concrete types or parameterize your struct:

struct MyStruct
    xs::Vector{Int}  # Concrete element type
end

struct AnotherStruct{T<:AbstractVector{Int}}
    data::T  # Parameterized field allows concrete types
end
Tip

If you intentionally use abstract field types (e.g., in cases where data types are inherently only known at compile time[nospecialize_tip]), you can suppress this diagnostic using pattern-based configuration:

[[diagnostic.patterns]]
pattern = "`MyStruct` has abstract field `.*`"
match_by = "message"
match_type = "regex"
severity = "off"

Unsupported feature (toplevel/unsupported-feature)

Default severity: Warning

Reported when JETLS loads code that uses a feature it does not support. JETLS continues the analysis with an approximation of the code, so diagnostics for the affected code may not match its actual behavior.

Currently, this is reported for include calls with a mapexpr function: JETLS analyzes the included file without applying the function.

Example:

include(rewrite, "generated.jl")  # JET analyzes this code approximately because it uses an unsupported feature.
                                  #
                                  # JET does not support `include(mapexpr::Function, filename::String)`.
                                  # The included file is analyzed without applying `mapexpr`.
                                  # (JETLS toplevel/unsupported-feature)

Analysis skipped (toplevel/analysis-skipped)

Default severity: Warning or Information

Reported when full analysis skips a file, either at the top of the file or at the include call that would load it. The severity depends on why the file is skipped:

  • Warning: At the top of package extension files (under a package's ext directory), since full analysis of extensions is not supported yet. This causes jetls check to fail by default when such files are passed to it.
  • Information: At the top of a file under a package's src or test directory that no include from the entry file of its analysis unit (src/<name>.jl or test/runtests.jl) reaches. The message names the entry file. Such a file is often no longer used. JETLS reports this once an analysis of the unit completes without a top-level error, since an analysis that stops early may not reach every file the unit includes.
  • Information: For files skipped because of analysis_overrides:
    • At the top of an excluded file. The message names the matched pattern.
    • At the top of a file of a package or its tests whose entry file (src/<name>.jl or test/runtests.jl) is excluded, since its analysis unit cannot be loaded without the entry file. The message names the entry file.
    • At an include call of an excluded file whose override sets on_include = "skip". Full analysis continues with the rest of the analysis unit without loading the file, so code after the include that depends on its definitions may get false diagnostics such as inference/undef-global-var. The diagnostic links to the excluded file as related information.

Skipped files receive no code-loading or inference diagnostics, apart from this one. They also have no lowering diagnostics that require their module context, such as lowering/undef-global-var and lowering/unused-import (see Files without full analysis).

Warning

The reports for analysis_overrides exist to make its effect visible. Like analysis_overrides itself, which is provided as a temporary workaround, they are experimental and may be removed or changed at any time.

Example (ext/MyPkgSomeDepExt.jl):

module MyPkgSomeDepExt  # JETLS does not support full analysis of package extensions yet.
                        #
                        # This file is not analyzed, so diagnostics that require full analysis are not reported for it.
                        # (JETLS toplevel/analysis-skipped)

using MyPkg, SomeDep

end

Example (test/runtests.jl, where each test file runs its own test set and the later tests do not depend on test/test_expensive.jl), with the following .JETLSConfig.toml:

[[initialization_options.analysis_overrides]]
path = "test/test_expensive.jl"
on_include = "skip"
using Test

@testset "MyPkg.jl" begin
    @testset "basics" include("test_basics.jl")
    @testset "expensive" include("test_expensive.jl")  # Full analysis skipped this `include` because the file is excluded by `analysis_overrides`.
                                                       #
                                                       # The definitions in the excluded file `/path/to/test/test_expensive.jl` are not available to the code after this point.
                                                       # (JETLS toplevel/analysis-skipped)
    @testset "utilities" include("test_utilities.jl")
end

Analysis stopped (toplevel/analysis-stopped)

Default severity: Warning

Reported at an include call when full analysis reaches a file excluded by analysis_overrides with on_include = "stop" (the default). Full analysis stops the whole analysis unit before loading the file, rather than skipping the include and continuing with missing definitions.

Code after that point and method bodies are not analyzed. Diagnostics from top-level code before it are kept, and other analysis units are unaffected. Hiding this warning does not resume analysis; set on_include = "skip" for the override to continue the analysis instead (see toplevel/analysis-skipped). Since its default severity is Warning, it causes jetls check to fail by default.

Warning

This diagnostic exists to make the effect of analysis_overrides visible. Like analysis_overrides itself, which is provided as a temporary workaround, it is experimental and may be removed or changed at any time.

Example (main.jl), with the following .JETLSConfig.toml:

[[initialization_options.analysis_overrides]]
path = "generated.jl"
include("generated.jl")  # Full analysis of this unit stopped because a file is excluded by `analysis_overrides`.
                         #
                         # The excluded file `/path/to/generated.jl` was not analyzed. Code after this point and method bodies are not analyzed.
                         # (JETLS toplevel/analysis-stopped)

Inference diagnostic (inference/*)

Inference diagnostics come from JET.jl's type inference during full analysis, at line granularity (see Type inference).

Each corresponds to a concrete Julia runtime error that JET detects statically, before the code runs:

In addition, inference/failing-test reports a @test that always fails.

Undefined global variable (inference/undef-global-var)

Default severity: Warning

References to undefined global variables, detected through full analysis. This diagnostic can detect comprehensive cases including qualified references (e.g., Base.undefvar). Position information is reported on a line basis.

Example:

function undef_global_var(x)
    Base.Math.sinkernel(x)  # `Base.Math.sinkernel` is not defined (JETLS inference/undef-global-var)
end

For faster feedback while editing, see lowering/undef-global-var (source: JETLS/live), which reports a subset of undefined variable cases with accurate position information.

Field error (inference/field-error)

Default severity: Warning

Access to non-existent struct fields. This diagnostic is reported when code attempts to access a field that doesn't exist on a struct type.

Example:

struct MyStruct
    property::Int
end
function field_error()
    x = MyStruct(42)
    return x.propert  # FieldError: type `MyStruct` has no field `propert`, available fields: `property` (JETLS inference/field-error)
end

Bounds error (inference/bounds-error)

Default severity: Warning

Out-of-bounds field access by index. This diagnostic is reported when code attempts to access a struct field using an integer index that is out of bounds, such as getfield(x, i) or tuple indexing tpl[i].

Note

This diagnostic is not reported for arrays, since the compiler doesn't track array shape information.

Example:

function bounds_error(tpl::Tuple{Int})
    return tpl[2]  # BoundsError: attempt to access Tuple{Int64} at index [2] (JETLS inference/bounds-error)
end

Method error (inference/method-error)

Default severity: Warning

Function calls where no unique matching method can be found for the inferred argument types. This diagnostic detects potential MethodErrors that would occur at runtime.

When no method matches, the diagnostic explains that the function exists but has no method for the inferred combination of argument types. It also includes up to three closest candidates:

sin('4')  # MethodError: no matching method found `sin(::Char)`
          #
          # The function `sin` exists, but no method is defined for this combination of argument types.
          #
          # Closest candidates are:
          # - `sin(!Matched::ComplexF16)`
          #   @ Base math.jl:1527
          # - `sin(!Matched::BigFloat)`
          #   @ Base mpfr.jl:946
          # - `sin(!Matched::Missing)`
          #   @ Base math.jl:1548
          # - ...
          # (JETLS inference/method-error)

When multiple union-split signatures fail to find matches, the diagnostic will report all failed signatures:

only_int(x::Int) = 2x

function union_split_method_error(x::Union{Int,String})
    return only_int(x)  # MethodError: no matching method found `only_int(::String)` (1/2 union split)
                        #
                        # The function `only_int` exists, but no method is defined for this combination of argument types.
                        #
                        # Closest candidates are:
                        # - `only_int(!Matched::Int64)`
                        #   @ Main Untitled-30064817009:1
                        # (JETLS inference/method-error)
end

Passing a keyword argument that the called method does not accept, which raises a MethodError at runtime, is also reported under this code:

kwfunc(; kw1=nothing, kw2=nothing) = (kw1, kw2)

kwfunc(; kw3=42)  # unsupported keyword argument `kw3` in `kwfunc(; kw3::Int64)`
                  # (JETLS inference/method-error)

Ambiguous calls are identified separately and include the conflicting methods and, when available, a possible disambiguating signature:

ambiguous(x, y::Int) = x + y
ambiguous(x::Int, y) = x - y

ambiguous(1, 2)  # MethodError: `ambiguous(::Int64, ::Int64)` is ambiguous.
                 #
                 # Candidates:
                 # - `ambiguous(x::Int64, y)`
                 #   @ Main Untitled-30064817009:7
                 # - `ambiguous(x, y::Int64)`
                 #   @ Main Untitled-30064817009:6
                 #
                 # Possible fix, define: `ambiguous(::Int64, ::Int64)`
                 # (JETLS inference/method-error)

Within a union-split call, ambiguous branches are reported separately from branches where no method matches.

Undefined keyword (inference/undef-keyword)

Default severity: Warning

Calls that omit a required keyword argument (a keyword declared without a default value). Julia raises an UndefKeywordError at runtime in this case.

Examples:

required_keyword(pos; key) = (pos, key)

required_keyword(42)  # missing keyword argument `key` (JETLS inference/undef-keyword)

To avoid false positives, this is only reported when the keyword is statically known to be missing on every path; calls that forward keywords dynamically (e.g. f(; kwargs...)) are not flagged.

Type error (inference/type-error)

All inference/type-error/* codes report a TypeError raised at runtime. They share the inference/type-error/ prefix, so a configuration pattern like inference/type-error/.* adjusts the whole group at once.

Keyword argument type error (inference/type-error/keyword)

Default severity: Warning

Keyword arguments passed with a value whose type does not match the keyword's declared type. Julia inserts a type assertion in the keyword sorter and raises a TypeError at runtime in this case.

Examples:

typed_keyword(; key::Int=0) = key

typed_keyword(; key=1.0)  # TypeError: in keyword argument `key`, expected `Int64`, got a value of type `Float64`
                          # (JETLS inference/type-error/keyword)

To avoid false positives, this is only reported when the value type can never match the declared type; calls where the value may match for some runtime values are not flagged.

Type assertion error (inference/type-error/type-assert)

Default severity: Warning

Type assertions that are guaranteed to fail. Julia raises a TypeError at runtime when a value does not satisfy a type assertion like x::T.

Example:

let x = rand()
    o = x::Int  # TypeError: in typeassert, expected Int64, got a value of type Float64
                # (JETLS inference/type-error/type-assert)
    o
end

This diagnostic is reported when inference can prove that the asserted value's inferred type has no overlap with the asserted type. Type assertions that may succeed for some runtime values are not reported.

Non-boolean condition (inference/type-error/non-bool-cond)

Default severity: Warning

Non-boolean values used in boolean context, such as if or while conditions. Julia requires conditions to be strictly Bool; using other types will raise a TypeError at runtime.

Examples:

function non_boolean_example()
    x = 1
    if x  # non-boolean `Int64` found in boolean context
          # (JETLS inference/type-error/non-bool-cond)
        return "truthy"
    end
end

When union-split types include non-boolean branches:

function is_enabled(flags::Dict{String,Bool}, name::String)
    if get(flags, name, nothing)  # non-boolean `Nothing` found in boolean context (1/2 union split)
                                  # (JETLS inference/type-error/non-bool-cond)
        return true
    end
    return false
end
Possibly `missing` call results

Calls with loosely typed arguments can be inferred to return missing even though the code rarely sees it: with x inferred as Any, x == :flag also matches ==(::Missing, ::Any) and is inferred as Union{Bool,Missing}. JETLS does not report errors caused only by such results. It still reports them when the argument types of the call involve Missing, e.g. branching on xs[i] == 0 for xs::Vector{Union{Missing,Int}}. As a limitation, branching on a call inferred as Union{Bool,Missing} is not reported when its argument types do not involve Missing, even if the callee itself returns missing:

parse_flag(s::String) = s == "yes" ? true : s == "no" ? false : missing

function use_flag(s::String)
    if parse_flag(s)  # not reported, although `parse_flag("maybe")` is `missing`
        return 1
    end
    return 2
end

Such a result is treated as Any from then on, so other uses of it, e.g. passing it to a function that accepts neither Bool nor Missing, are not checked either.

Legacy diagnostic code (inference/non-boolean-cond)

inference/non-boolean-cond is a deprecated code alias for inference/type-error/non-bool-cond. It is kept for existing documentation links and diagnostic pattern configurations for now, but this compatibility support may be removed in a future release.

Failing test (inference/failing-test)

Default severity: Warning

@tests whose expression is inferred to always evaluate to false or to a non-boolean value. Test.jl records such a test as a failure whenever it runs.

Examples:

using Test

@testset "example" begin
    s = string(42)
    @test s isa Int  # this test always fails since its expression evaluates to `false`
                     # (JETLS inference/failing-test)
end

@testset "example" begin
    v = [1, 2, 3]
    @test length(v)  # this test always fails since its expression evaluates to non-boolean `Int64`
                     # (JETLS inference/failing-test)
end

Only the tests in top-level code, including the code in @testsets, are reported, not those in function bodies. Tests expected to fail, i.e. @test_broken and @test ex broken=true, are not reported. Note that @test false is reported as well, so use e.g. error("unreachable") to mark code that should not be reached.

JETLS analyzes test sets as if they were declared with failfast=true, where a failing test aborts the test set. So it analyzes the code after @test ex assuming that ex is true, and does not analyze the code after a test that always fails:

@testset "example" begin
    x = rand((0, nothing))
    @test x isa Int
    sin(x)  # not reported as `sin(::Nothing)`, since `x` is an `Int` here
    @test x isa String  # this test always fails since its expression evaluates to `false`
                        # (JETLS inference/failing-test)
    sin("julia")  # not analyzed
end

Test sets that specify failfast=false and test sets of a custom type, e.g. @testset MyTestSet "example" begin ... end, are analyzed as declared.

Configuration diagnostic (config/*)

Configuration diagnostics report issues in the .JETLSConfig.toml file (see Configuration).

The code actions for deprecated settings keep the rest of the file, including comments, as it is. When the file has several deprecated settings, the "Fix all deprecated settings" code action fixes them at once. When JETLS loads a configuration file with deprecated settings, it also lists them in a message with a "Fix all" action that applies the same fixes. This works even if your editor does not show JETLS diagnostics for TOML files.

Deprecated configuration key (config/deprecated-key)

Default severity: Warning

Reported on a deprecated key of .JETLSConfig.toml. JETLS reads a deprecated key that has a replacement as the replacement, and ignores one that has none, but the key may stop being recognized in a future release.

[testrunner]
executable = "testrunner"  # `testrunner.executable` is deprecated and no longer has any effect; please remove it from your config.
                           # (JETLS config/deprecated-key)
Code action available

Use the "Remove deprecated testrunner.executable" code action to remove the key, along with any table it leaves empty. For a key that has a replacement, the "Replace … with …" code action moves its value to the new key instead.

Deprecated configuration value (config/deprecated-value)

Default severity: Warning

Reported on a deprecated value of a .JETLSConfig.toml setting. JETLS reads the value as its replacement, but the value may stop being accepted in a future release.

[full_analysis]
auto_instantiate = true  # `true` for `full_analysis.auto_instantiate` is deprecated; use `"always"` instead.
                         # (JETLS config/deprecated-value)
Code action available

Use the "Replace true with "always"" code action to replace the value.

TestRunner diagnostic (testrunner/*)

TestRunner diagnostics come from manually running tests via the TestRunner integration, at line granularity (see Test execution).

Test failure (testrunner/test-failure)

Default severity: Error

Test failures reported by TestRunner integration that happened during running individual @testset blocks or @test cases.

Note

Diagnostics from @test cases automatically disappear after 10 seconds, while @testset diagnostics persist until you run the testset again, restructure testsets, or clear them manually.

Configuration

You can configure which diagnostics are shown and at what severity level under the [diagnostic] section. This allows you to customize JETLS's behavior to match your project's coding standards and preferences.

Common use cases

Suppress specific macro expansion errors:

[[diagnostic.patterns]]
pattern = "Macro name `MyPkg.@mymacro` not found"
match_by = "message"
match_type = "literal"
severity = "off"

Apply different settings for test files:

# Downgrade unused arguments to hints in test files
[[diagnostic.patterns]]
pattern = "lowering/unused-argument"
match_by = "code"
match_type = "literal"
severity = "hint"
path = "test/**/*.jl"

# Disable all diagnostics for generated code
[[diagnostic.patterns]]
pattern = ".*"
match_by = "code"
match_type = "regex"
severity = "off"
path = "gen/**/*.jl"

Disable unused variable warnings during prototyping:

[[diagnostic.patterns]]
pattern = "lowering/(unused-argument|unused-local|unused-assignment)"
match_by = "code"
match_type = "regex"
severity = "off"

Make inference diagnostic less intrusive:

[[diagnostic.patterns]]
pattern = "inference/.*"
match_by = "code"
match_type = "regex"
severity = "hint"

For complete configuration options, severity values, pattern matching syntax, and more examples, see the [diagnostic] configuration section in the JETLS configuration page.