Configuration

This page is the authoritative specification for the three TOML files that configure the Julia tooling stack:

FileConfiguresConsumed by
JuliaLint.tomlWhich diagnostics are reported, and at what severityThe language server, julialint
JuliaFormat.tomlFormatting style and optionsThe language server, juliaformat
JuliaTestItems.tomlWhich files are searched for test itemsThe language server, the test runners

The three files deliberately share one grammar: the same discovery rule, the same include/exclude globs, the same [[override]] mechanism. Learn it once and it applies everywhere. The shared machinery lives in src/config_common.jl.

Shared mechanism

Discovery: the nearest file governs, wholesale

To resolve configuration for a file, JuliaWorkspaces walks up from that file's directory and uses the first config file of the relevant kind it finds. That file then applies as a whole:

  • Keys it does not set take their built-in defaults.
  • They never take a value from a config file further up the tree.
  • If no config file is found anywhere above the file, pure defaults apply.

There is no merging across files. This is the single most important thing to understand about the format, and it is a deliberate departure from the earlier per-key hierarchical merge. Given

myproject/
  JuliaLint.toml        # preset = "strict", exclude = ["gen/**"]
  src/
    JuliaLint.toml      # [rules] unused_binding = "off"
    a.jl

src/a.jl is governed only by src/JuliaLint.toml. It gets the default preset (not strict), no exclusions, and unused_binding off. The root file is irrelevant to it.

The rationale is predictability: to know how a directory is configured you read exactly one file, rather than reconstructing a merge across an arbitrary number of them. The cost is that a nested config must restate anything it wants to keep — copy the parent file and edit it.

Because of that cost, a nested config file is a last resort, not a normal way to vary settings by directory. The normal setup is a single config file of each kind at the repository root; when a subtree needs different settings, use an [[override]] block in that one file — it changes only the keys it names, while a nested file silently resets everything it does not restate back to the defaults. Reach for a nested file only when a subtree is genuinely independent of the enclosing project and should not follow its configuration at all — a vendored repository with its own conventions, say.

Config file names are matched case-insensitively on the basename, so JuliaLint.toml and julialint.toml both work. A leading dot does not: .JuliaLint.toml is not recognised.

Precedence within a file

built-in defaults  <  preset / style  <  top-level keys  <  last matching [[override]]

File selection: include and exclude

Every config file accepts two top-level glob lists, relative to the directory holding the config file:

include = ["src/**", "test/**"]
exclude = ["**/generated_*.jl"]
  • An empty or absent include selects everything.
  • exclude always wins over include.
  • An excluded file is not linted / formatted / searched for test items at all.

A config file always validates itself even when its own globs exclude the directory it lives in — otherwise a mistake in exclude could hide the very diagnostic that would explain it.

Glob syntax

Gitignore-style, implemented by GlobPattern:

PatternMatches
*Any run of characters within one path segment
**Any number of path segments
?A single character, not a separator
[abc], [!abc]A character class, optionally negated
foo/ (trailing slash)Everything below the foo directory
/foo.jl (leading slash)Anchored to the config file's directory
foo.jl (no separator)Matches at any depth, like gitignore

Paths are normalised to / before matching, so test\** and test/** behave identically. Matching is case-insensitive on Windows.

Path-scoped overrides

Overrides are the mechanism for giving part of a tree different settings. Any config file may carry repeated [[override]] blocks. Each takes a required paths glob list and re-scopes a subset of the file's own keys to the files those globs match. Later blocks win over earlier ones.

[rules]
unused_binding = "error"

[[override]]
paths = ["test/**"]

[override.rules]
unused_binding = "off"

This covers the common "different settings for tests" case — and anything else that would tempt one to add a second config file. Prefer an override whenever the subtree is still part of the same project: it varies exactly the keys it names, where a nested config file would have to restate everything else it wants to keep.

config-version

Every file accepts an optional config-version integer. The current format is version 1, and an absent key means 1.

It is reserved from the first release rather than added when first needed: a released tool that does not know the key can only report it as an unknown key when it meets a file written for a later format, and that cannot be fixed retroactively in copies already installed. A file declaring a version this tool does not understand is told to upgrade the tooling.

Superseded configuration

Because the nearest config governs wholesale, a config file in a subdirectory does not extend the one above it — it replaces it, and since nested config files are discouraged (use [[override]] instead), that replacement is more often an accident than a decision. It is also silent by nature, so a config file with another of the same kind in an enclosing directory reports a shadowed_config diagnostic (info by default) naming the file it takes over from.

This is the price of choosing nearest-wins over cascading. The alternative is that a config dropped into a subdirectory — a vendored repository, a copied example, a half-finished subpackage extraction — quietly voids the project's own configuration for that subtree. It is an ordinary rule, so a project that genuinely wants independent subtrees sets shadowed_config = "off".

Validation

Unknown keys and invalid values are reported as diagnostics on the config file itself, under the config_errors rule. Keys from the previous flat schema are recognised specially and reported with the name of their replacement rather than a bare "invalid key", so an existing config tells its owner what to write instead.

JuliaLint.toml

preset = "default"
include = ["**/*.jl"]
exclude = ["gen/**"]

[rules]
unused_binding = "warning"
nothing_comparison = "error"
index_from_length = "off"
missing_reference = { severity = "warning", scope = "symbols" }

[[override]]
paths = ["test/**"]

[override.rules]
unused_binding = "off"

Rule ids

A rule is the unit a user enables, disables, or re-prioritises. Rule ids are the stable public contract of the linter: they appear in this file, on Diagnostic.code, as the LSP diagnostic code, and as the SARIF ruleId in julialint --format sarif (which is what makes per-rule suppression in GitHub Code Scanning work).

A rule usually groups several internal StaticLint.LintCodes members that a user would want to configure together — nothing_comparison covers both NothingEquality and NothingNotEq. The mapping is declared once in src/lint_rules.jl as LINT_RULES; LINTCODE_TO_RULE inverts it.

Severities

Every rule takes one of:

"off" · "hint" · "info" · "warning" · "error"

Severity is a single mechanism doing three jobs: "off" disables a rule, the middle values control how an editor renders it, and "error" makes julialint exit non-zero. There is no separate enable/disable list.

The configured severity replaces the built-in one. Diagnostic tags do not follow it: an unused binding stays tagged unnecessary (so editors grey it out) whether you report it as a hint or an error, because the tag describes the finding, not its importance.

Rules with options

A rule that takes parameters is written as a table instead of a bare string, with the severity under the reserved severity key:

[rules]
missing_reference = { severity = "warning", scope = "symbols" }

# or, equivalently
[rules.missing_reference]
severity = "warning"
scope = "symbols"

Omitting severity keeps the preset's value while still setting options. No rule option may be named severity; this is enforced by the validator.

Presets

A preset is a named severity baseline, nothing more — [rules] entries are deltas applied on top of it.

PresetIntent
minimalOnly outright breakage: syntax, test item, TOML and config errors, include-graph problems, and invalid const declarations. Everything else off.
defaultThe out-of-the-box behaviour.
strictEvery rule on, with hints and informational findings promoted to warnings.

Because a preset is just a Dict{Symbol,Symbol}, adding one later needs no new mechanism.

A preset name floats: it tracks the tool rather than pinning a frozen rule set, so upgrading the tooling can change what a preset reports. To keep that from breaking projects on upgrade, a rule that did not exist before enters existing presets as "off"; promoting it is a deliberate, changelogged change. Version-pinning syntax (preset = "default@2") may be added later — bare names will keep floating, so nothing written today changes meaning.

Every preset must classify every rule. This is enforced when lint_rules.jl loads, so a rule added without a decision fails the build rather than appearing in everyone's default at whatever severity a fallback happened to pick.

The rules

RuleDefaultReports
syntax_errorserrorJulia syntax errors
syntax_warningsoffJulia syntax warnings
testitem_errorserrorMalformed @testitem blocks
toml_syntax_errorserrorTOML syntax errors in config, Project.toml, Manifest.toml
config_errorserrorInvalid keys/values in any of the three config files
shadowed_configinfoA config file that supersedes another of the same kind in an enclosing directory
environment_errorsinfoA project/test environment that could not be resolved, reported on its Project.toml
incorrect_call_argsinfoWrong argument count/type; calls to method-less functions
incorrect_iter_specinfoLoop iterators that will likely error
index_from_lengthinfoIndexing off length/size instead of eachindex/axes
nothing_comparisoninfo== nothing / != nothing instead of isnothing/===
const_if_conditioninfoBoolean literal or unbracketed assignment as an if condition
pointless_booleaninfo&&/|| whose first argument is a boolean literal
invalid_type_declarationinfoNon-DataType in a type declaration
unused_type_parameterhintDeclared but unused type parameters
module_nameinfoA module named after its parent
type_piracyinfoType piracy; overloading != instead of ==
unused_function_argumenthintDeclared but unused function arguments
duplicate_function_argumentinfoRepeated argument names in a signature
kw_default_mismatchinfoKeyword defaults not matching the argument type
literal_useinfoInappropriate use of literal values
break_continueinfobreak/continue outside a loop
global_const_declinfoType declarations on globals; const on locals
const_declinfoInvalid const declarations and redefinitions
unused_bindinghintVariables assigned but never used
relative_importinfoA relative import with more dots than available nesting
include_errorswarningCircular, duplicate, missing or unreadable includes
missing_referencewarningUnresolved references. Option scope: "none", "symbols", "all" (default)
unresolved_importwarningImports whose target could not be resolved

Rules and code actions

A quick fix is withdrawn when the rule it fixes is turned off: with no diagnostic to act on, offering to "fix" it would contradict the user. Refactorings and source actions (ExpandFunction, the raw-string rewrites, the docstring actions) fix no rule and are never affected by lint configuration — they are editor capabilities, not fixes.

The link is declared by the rule field of _ActionDef in src/layer_actions.jl; nothing means "not a fix for anything".

JuliaFormat.toml

style = "minimal"
include = ["**/*.jl"]
exclude = ["gen/**"]

[options]
margin = 92
always_for_in = true

[[override]]
paths = ["docs/**"]

[override.options]
margin = 80

style is the preset and [options] are deltas on top of it — the same shape as preset and [rules] in the lint file.

KeyDefaultValues
style"minimal"default, yas, blue, sciml, minimal, runic
[options]Any field of JuliaFormatter.Options

The runic style accepts no options; combining it with a non-empty [options] is reported as a configuration error rather than silently ignored.

An excluded file is not a formatting failure. Callers formatting many files should ask is_format_excluded first and skip, rather than letting get_format_edits report it as an error.

Relation to .JuliaFormatter.toml

JuliaWorkspaces never reads JuliaFormatter.jl's own .JuliaFormatter.toml. JuliaFormatter.format_text is always called with an explicit option set derived solely from JuliaFormat.toml. The files are deliberately distinct: one file interpreted by two independently versioned tools would diverge silently.

JuliaTestItems.toml

include = ["src/**", "test/**"]
exclude = ["test/manual/**"]

Version 1 is discovery scope only — it controls which files are searched for @testitem blocks. Execution settings (worker counts, timeouts, environment variables, tag filters, per-item defaults) are deliberately not part of it yet; they will arrive as additional sections under the same grammar.

Note the division of labour: this file decides where test items are found, while the testitem_errors rule in JuliaLint.toml decides whether malformed test items are reported as diagnostics.

Implementation notes

Query structure

Configuration resolution is split into two Salsa queries per file kind so that parsing happens once per config file rather than once per configured file:

  • derived_parsed_lint_config(rt, config_uri) — parses one JuliaLint.toml into a ParsedLintConfig (preset, rule table, filters, override blocks).
  • derived_effective_lint_config(rt, uri) — finds the governing config, then resolves preset < [rules] < overrides for that one file, yielding an EffectiveLintConfig.

Editing a config file invalidates only through derived_toml_syntax_tree; adding or removing one invalidates through derived_text_files. Formatting has the same shape via derived_format_configuration, as does test-item discovery via derived_testitems_selected.

Every value reachable from a config struct has well-defined == and hash (GlobPattern compares by its written pattern, not its compiled regex) so that Salsa can backdate correctly when a config edit turns out not to change the effective result.

Where rules are applied

StaticLint.LintOptions — the 14 boolean gate that check_all consults — is derived from the effective config by lint_options_from_config: a category is enabled when any rule mapping into it is not "off". Because several rules can share one category, and several rules have no category at all, that gate is necessarily coarse.

The precise per-rule decision therefore happens where diagnostics are emitted, in _emit_hint_diagnostics! (src/lint_emission.jl): each finding's rule id is looked up, an "off" rule is skipped, and the configured severity replaces the built-in one. This keeps rule granularity independent of StaticLint's internal check structure, at the cost of computing a small number of findings that are then discarded.

_emit_hint_diagnostics! is shared by both static-lint pipelines — the whole-closure pass in layer_static_lint.jl and the per-file pass in layer_file_analysis.jl — which differ only in how a call mismatch is described and in the container they collect into.

Migrating from the old schema

The previous flat schema (static-lint, nothingcomp, missing-refs, break-continue, …) is not honoured. Every old key produces a diagnostic naming its replacement:

OldNew
static-lint = falsepreset = "minimal", or individual [rules] entries
syntax-errors = false[rules] syntax_errors = "off"
nothingcomp = false[rules] nothing_comparison = "off"
missing-refs = "symbols"[rules] missing_reference = { scope = "symbols" }
useoffuncargs = false[rules] unused_function_argument = "off"
break-continue = false[rules] break_continue = "off"

The full table is _LINT_CONFIG_MIGRATIONS in src/layer_diagnostics.jl. For the formatter, top-level option keys move into [options].

Because old keys are inert rather than an error, a project that relied on static-lint = false will start reporting lint diagnostics again after upgrading until its config is migrated.