Analysis

JETLS analyzes your code in two layers:

  • Full analysis runs when you open or save a file. It loads your code, either as a package or as a script, and runs type inference on it, reporting toplevel/* and inference/* diagnostics.
  • Live analysis runs as you edit. It parses and lowers each file, reporting syntax/* and lowering/* diagnostics, and powers language features such as completion and go-to-definition.

The two layers are connected by the module context: full analysis records the module each part of a file is evaluated in, and live analysis uses it to resolve the macros and global names that the file refers to. The following diagram summarizes this structure:

Full analysis establishes the module context that live analysis uses

Full analysis establishes the module context that live analysis uses

Full analysis describes how JETLS loads your code and which files it covers. Live analysis describes how live analysis depends on full analysis, including what remains available for files that full analysis does not cover.

Full analysis

Full analysis consists of two steps:

Both kinds of diagnostics are delivered through the JETLS/save source.

Full analysis works on analysis units. Each unit starts from an entry file, such as the entry file of a package or a script, and covers the entry file and every file reachable from it through include.

Security

Full analysis runs your top-level code and the dependency packages it loads, both of which can execute arbitrary code. Live analysis can also execute macro code during macro expansion. Neither layer is a sandbox; do not run JETLS on code you do not trust.

Full analysis modes

Full analysis loads an analysis unit in one of two ways. In both, the dependency packages your code loads are loaded unconditionally. The comments in the following example show which top-level code each mode executes:

using Statistics          # package: executed, script: executed
data = rand(10)           # package: executed, script: not executed
struct Point              # package: executed, script: executed
    x::Float64
end
norm2(p::Point) = p.x^2   # package: executed, script: executed
println(mean(data))       # package: executed, script: not executed

Package analysis

Package analysis executes all top-level code of a package unconditionally, then runs type inference on the loaded code. In the example above, it also runs println(mean(data)), so an error in such code is reported as toplevel/error.

Script analysis

Script analysis loads a script with partial execution: it executes only the top-level code needed to load definitions such as types, methods, and macros, and runs type inference on the rest without running it. In the example above, it defines Point and norm2 but only infers data = rand(10) and println(mean(data)), so an error in such code is reported as an inference/* diagnostic.

Since script analysis does not execute such assignments, a definition that needs the value of the assigned binding cannot be loaded unless that value can be inferred. Otherwise, JETLS reports toplevel/missing-concretization:

read_config() = Base.get_bool_env("USE_FLOAT64", true; throw=true)
data = rand(read_config() ? Float64 : Float32, 10)
struct Samples
    values::typeof(data)  # toplevel/missing-concretization
end

See [full_analysis] concretization_patterns for how to let JETLS evaluate such bindings.

How each file is analyzed

JETLS classifies a file by the nearest Project.toml found by searching upward from the file's directory, and by the file's location relative to it. Here, a package is a directory whose Project.toml has a name entry. The class determines the entry file of the analysis unit the file belongs to, the analysis mode that loads the unit, and the environment the unit is loaded in:

FileEntry fileAnalysis modeEnvironment
Under src/ of a packagesrc/<name>.jlPackage analysisThe package environment
Under test/ of a packagetest/runtests.jlScript analysisThe package test environment
Under ext/ of a package—No full analysis—
Elsewhere in an environmentThe opened fileScript analysisThe nearest environment
Without a Project.tomlThe opened fileScript analysisNo project

An analysis unit consists of its entry file and the files reached through include from it. Some documents are handled differently; see Unsaved buffers and notebooks, Files outside the workspace root, and jetls check.

Package source files

Files under the src/ directory of a package are analyzed together through the package entry file, src/<name>.jl, or the file given by the entryfile entry of the Project.toml, with the package analysis mode in the package environment. Files under src/ that the package does not include are not covered by full analysis, and JETLS reports toplevel/analysis-skipped for them (see Files without full analysis).

Package test files

Files under the test/ directory of a package are analyzed together through test/runtests.jl, with the script analysis mode in the package test environment: the environment of test/Project.toml if the package has one without a name entry, e.g. when the tests are a [workspace] member of the package, and the package environment otherwise. Files under test/ that test/runtests.jl does not include are not covered by full analysis, and JETLS reports toplevel/analysis-skipped for them (see Files without full analysis).

Package extension files

Files under the ext/ directory of a package are not covered by full analysis. JETLS reports toplevel/analysis-skipped for them (see Files without full analysis).

Scripts

The following files are analyzed as scripts, with the script analysis mode:

  • Files in a package outside src/, test/, and ext/
  • Files in an environment whose Project.toml has no name entry, unless that environment is a package test environment
  • Files without a Project.toml above them

A script is analyzed together with the files it includes. Opening a script that no earlier analysis covers makes it the entry file of a new unit. For example, if scripts/main.jl includes scripts/utils.jl, opening scripts/main.jl analyzes both files together, while opening scripts/utils.jl first analyzes it alone, without what scripts/main.jl defines before includeing it. Once scripts/main.jl is analyzed, e.g. when you open it, scripts/utils.jl belongs to its unit: its diagnostics come from that analysis, and saving it reanalyzes the unit from scripts/main.jl.

A script is loaded in the environment of the nearest Project.toml. A script without one is analyzed as julia script.jl would run it: dependency packages are loaded from the default environment (e.g. @v1.13) and the standard library. When the JETLS process has an active project, set with --project given to Julia before -- or with the JULIA_PROJECT environment variable, they are loaded from it first, as julia --project script.jl would. The same applies to the other documents that are analyzed without a project, such as files opened without a workspace folder.

# Analyze a standalone script with the packages of an environment
jetls --project=/path/to/env -- check script.jl

Unsaved buffers and notebooks

Files outside the workspace root

Files outside the workspace root, such as the source of a dependency opened through go-to-definition, are not covered by full analysis (see Files without full analysis).

When a file is opened without a workspace folder, there is no workspace root, and the file is analyzed as a script without a project, regardless of the Project.toml files above it.

jetls check

jetls check classifies files in the same way, except that it also analyzes files outside the root path (see Input paths and analysis mode).

When full analysis runs

Full analysis works on whole units. JETLS requests it in the following cases:

  • When you open a file that no full analysis covers yet. Opening a file that an earlier analysis already covers, e.g. another file of an analyzed package, reuses that result.
  • When you save a file. JETLS schedules reanalysis of the unit the file belongs to, such as the entire package for a package source file. Saves are debounced per unit: JETLS waits for [full_analysis] debounce seconds without another save in that unit before queuing the request.
  • For unsaved buffers, which are never saved, after each edit, with a fixed debounce of 3 seconds.
  • For notebooks, when the notebook is opened and when it is saved.
  • When [full_analysis] concretization_patterns changes, for every analyzed unit containing a file that an added or removed pattern applies to. Package source analysis is not rerun, since it evaluates all top-level code regardless of these patterns.
  • When [full_analysis] concretization_timeout changes, for every unit that has been analyzed.

Reanalysis is skipped if any previously analyzed file in the unit has a saved syntax error. This can prevent full-analysis diagnostics from updating even when you save another, syntactically valid file in the same unit. Fix the syntax errors and save the affected files to allow full analysis to run again.

The first full analysis in a package environment may require the environment to be instantiated. See [full_analysis] auto_instantiate for how JETLS handles that.

Excluding files from full analysis

The analysis_overrides initialization option excludes the matched files from full analysis:

[[initialization_options.analysis_overrides]]
path = "test/fixtures/**"

An excluded file reports toplevel/analysis-skipped at its first line, which names the matched pattern. If the entry file of a package or its tests is excluded, the other files of that analysis unit are not analyzed either, and report the same diagnostic naming the entry file.

If full analysis reaches an include of a matched file, it reports toplevel/analysis-stopped at the call site and stops the entire analysis unit before loading that file. Code after that point and method bodies are not analyzed; diagnostics from top-level code before the include are kept. Disabling the warning through diagnostic settings does not change this behavior.

When the code after the include does not depend on the matched file, set on_include = "skip" to continue the analysis without loading the file instead. Full analysis then reports toplevel/analysis-skipped at the call site and analyzes the rest of the unit as usual:

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

Excluded files are handled as described in Files without full analysis.

Warning

analysis_overrides is provided as a temporary workaround and may be removed or changed at any time.

Live analysis

Live analysis parses and lowers your code with JuliaSyntax.jl and JuliaLowering.jl, without loading the file as a package or script. Macro expansion can still execute macro code. Live analysis reports syntax/* and lowering/* diagnostics, delivered through the JETLS/live source, and powers most language features, such as completion, hover, go-to-definition, references, and rename.

Live analysis runs whenever the contents of an open file change. It also analyzes files you have not opened, as long as full analysis covers them, e.g. the other files of a package once you open one of them (see [diagnostic] all_files).

Since live analysis takes the module context from full analysis, some of its results depend on full analysis: the lowering/* diagnostics that depend on the module context, such as lowering/undef-global-var, are reported only once full analysis has established the module context of the file. JETLS then refreshes the live diagnostics to include them, so they appear without further edits. Global names are likewise resolved against the definitions that full analysis has loaded, except that completion of global names also offers the global names defined by the current contents of files in the same analysis unit. Hover and type inlay hints also run type inference on the current top-level form on demand in that context.

Files without full analysis

Full analysis does not cover the following files:

  • Files outside the workspace root, such as the source of a dependency opened through go-to-definition
  • Package extension files
  • Files that are not reachable through include from the entry file of any analysis unit, such as a file under src/ that the package does not include
  • Files excluded with analysis_overrides, and the files of a package or its tests whose entry file is excluded
  • Any file until its first full analysis completes

For these files, live analysis uses a fallback module context in place of the one full analysis would establish. The fallback context provides the names of Base and of the Test standard library, but none of the names the file itself imports. As a result:

  • syntax/* diagnostics and the lowering/* diagnostics that do not depend on the module context, such as unused or undefined local variables and unreachable code, are reported as usual.
  • The lowering/* diagnostics that depend on the module context, such as lowering/undef-global-var, lowering/unused-import, and lowering/macro-expansion-error, are not reported. Neither are inference/* diagnostics or toplevel/* diagnostics from code loading. For package extension files and files excluded with analysis_overrides, toplevel/analysis-skipped explains why the file is not analyzed.
  • Go-to-definition, references, and rename keep working for local variables, including inside @testset and other macros available in the fallback context. The arguments of other macro calls are analyzed approximately, as if they were written without the macro. For global names, these features only consider the file itself.
  • Completion of global names offers the names available in the fallback context and the global names that the file itself defines.
  • Hover and type inlay hints may give incomplete results.
  • unsaved_buffers

    How unsaved buffers are represented depends on the client. JETLS supports unsaved buffers with the following URI schemes:

    • untitled: (VSCode and VSCode-based editors)
    • buffer: (Sublime Text)