Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lint Rules

fatou lint runs a set of built-in rules over each file and reports a diagnostic for every finding. This page is the catalogue: one section per rule, keyed by its stable rule ID. That ID is what appears in a diagnostic, what [lint] select/ignore (and --select/--ignore) target, what [lint.severity] re-grades, and what a # fatou-ignore <id> comment suppresses.

Most rules are on by default. The few that are opt-in say so in their description; name one in select to run it.

Where a rewrite is unambiguous a rule carries an autofix: a safe fix (shown below as “After applying the fix”) is applied by fatou lint --fix; an unsafe fix, one that may change behavior, is applied only with --unsafe-fixes or as an editor code action, so it has no “after” block here.

Each example below is linted live to produce its diagnostics and fixed output, so this page never drifts from the rules’ actual behavior.

unused-binding

Flag a local variable that is assigned but never read in the same scope. Parameters, loop and catch variables, struct fields, type parameters, and top-level definitions are exempt, since those are meaningful even when unread. Names beginning with _ are skipped, following Julia’s throwaway convention.

tmp is assigned inside f but never used:

function f(x)
    tmp = x + 1
    return x
end
warning: unused-binding
 --> example.jl:2:5
  |
2 |     tmp = x + 1
  |     ^^^ local variable `tmp` is assigned but never used

unused-import

Flag an explicitly imported name that is never used: import X, import X as Y, and the colon-item forms using X: a / import X: a. The whole-module using X form is exempt, since it attaches exports that resolve elsewhere. A qualified use (X.f) or a re-export counts as a use.

sortperm is imported but never referenced:

using Base: sortperm, sum

println(sum([1, 2, 3]))
warning: unused-import
 --> example.jl:1:13
  |
1 | using Base: sortperm, sum
  |             ^^^^^^^^ `sortperm` is imported but never used

duplicate-argument

Flag the same parameter name declared more than once in a single signature. Julia rejects such a definition outright, so it is always a mistake. Positional and keyword parameters share one namespace.

x appears twice in the parameter list:

function dist(x, y, x)
    hypot(x, y)
end
error: duplicate-argument
 --> example.jl:1:21
  |
1 | function dist(x, y, x)
  |                     ^ argument name `x` is used more than once

duplicate-keyword-argument

Flag the same keyword argument supplied more than once at a call site. Julia rejects such a call at lowering (keyword argument "a" repeated in call to "h"), so it parses but cannot run. Keywords before and after the ; share one namespace, and the ;-block shorthand h(; a) counts as passing a. A keyword splat (h(; kw...)) names nothing the rule can read, so it neither triggers the finding nor silences it for the keywords written out. Calls inside quoted code or a macro call are exempt, since neither is lowered as written.

label is supplied twice in one call:

plot(xs, ys, label = "before", label = "after")
error: duplicate-keyword-argument
 --> example.jl:1:32
  |
1 | plot(xs, ys, label = "before", label = "after")
  |                                ^^^^^ keyword argument `label` is passed more than once

unused-argument

Flag a function parameter that is never read in its body. Every signature form is covered — long, short (f(x) = ...), anonymous, and do. All-underscore names (_, __) follow Julia’s throwaway convention and are skipped, and stub methods whose body is a single placeholder expression — a literal (f(x) = 0), nothing, or an error(...)/throw(...) call — are exempt. Because methods that dispatch on an argument’s type without reading its value are common, this rule is disabled by default; enable it with --select unused-argument.

factor is accepted but never used:

function scale(x, factor)
    2 * x
end
warning: unused-argument
 --> example.jl:1:19
  |
1 | function scale(x, factor)
  |                   ^^^^^^ function argument `factor` is never used

undefined-name

Flag an identifier that no resolution tier provides: not a local or a file binding, not a workspace sibling, not a whole-module using’s export, and not a Base/Core name. Such a read raises UndefVarError at runtime. The whole file is skipped when it evals, includes outside a known workspace, or usings a module the library cannot resolve — in those cases any name may exist; value reads inside macro calls and quoted code are likewise exempt. Off by default: the rule needs project context to be sound, so the language server enables it for workspace member files, while the CLI (resolving against a built-in Base/Core snapshot) leaves it opt-in for self-contained scripts.

raduis is a typo; no tier resolves it:

function area(radius)
    return pi * raduis^2
end
warning: undefined-name
 --> example.jl:2:17
  |
2 |     return pi * raduis^2
  |                 ^^^^^^ `raduis` is not defined

break-outside-loop

Flag a break or continue with no enclosing for or while loop. The code parses but always fails at lowering with “break or continue outside loop” — including inside a closure, do-block, or comprehension body defined within a loop, since break cannot cross a function boundary.

break with no loop in sight:

function process(x)
    if x < 0
        break
    end
    x
end
error: break-outside-loop
 --> example.jl:3:9
  |
3 |         break
  |         ^^^^^ `break` outside of a `for` or `while` loop

A do-block body is an anonymous function, so the outer loop is out of reach:

for i in 1:3
    foreach(1:2) do x
        continue
    end
end
error: break-outside-loop
 --> example.jl:3:9
  |
3 |         continue
  |         ^^^^^^^^ `continue` outside of a `for` or `while` loop

const-local

Flag a const declaration inside a local scope — a function or macro body, a let, a for/while body, a try, a closure, or a comprehension. const is only meaningful at global scope (the file top level and each module body); anywhere else the code parses but always fails at lowering with “unsupported const declaration on local variable”. A const field of a mutable struct is a different construct and is left alone, as is a const inside quoted code or a macro argument, which may never be lowered as written.

const inside a function body:

function scale(x)
    const factor = 2
    factor * x
end
error: const-local
 --> example.jl:2:5
  |
2 |     const factor = 2
  |     ^^^^^^^^^^^^^^^^ `const` declaration on a local variable

A let body is local too — the declaration belongs at top level:

let
    const limit = 10
    limit
end
error: const-local
 --> example.jl:2:5
  |
2 |     const limit = 10
  |     ^^^^^^^^^^^^^^^^ `const` declaration on a local variable

global-const-in-function

Flag a global const declaration inside a function — a function or macro body, a short-form definition, a closure, a do block, or a comprehension. Julia allows global const at global scope and in a soft local scope such as a let, a loop body, or a try, but inside a function the code parses and then always fails at lowering with “global const declaration not allowed inside function”. A soft scope nested inside a function does not help. Both spellings of the modifier are flagged, and a declaration inside quoted code or a macro argument is left alone, since it may never be lowered as written. An unmodified const in a local scope is const-local’s finding.

global const inside a function body:

function setup()
    global const LIMIT = 10
end
error: global-const-in-function
 --> example.jl:2:5
  |
2 |     global const LIMIT = 10
  |     ^^^^^^^^^^^^^^^^^^^^^^^ `global const` declaration inside a function

A nested soft scope does not rescue it — the enclosing function still owns the declaration:

function setup()
    for i in 1:3
        global const LIMIT = i
    end
end
error: global-const-in-function
 --> example.jl:3:9
  |
3 |         global const LIMIT = i
  |         ^^^^^^^^^^^^^^^^^^^^^^ `global const` declaration inside a function

local-const

Flag a const declaration carrying a local modifier, in either order. Julia has no local const construct: the code parses but always fails at lowering with “expected assignment after const”, everywhere — the file top level, a module or struct body, a loop or let, and inside a function alike. Write local z = 1 for a local binding or const z = 1 for a constant. A declaration inside quoted code or a macro argument is left alone, since it may never be lowered as written.

local and const cannot be combined:

local const LIMIT = 10
error: local-const
 --> example.jl:1:1
  |
1 | local const LIMIT = 10
  | ^^^^^^^^^^^^^^^^^^^^^^ `local const` declaration is not supported

The other order is the same construct, and a function body is no different from the top level:

function scale(x)
    const local factor = 2
    factor * x
end
error: local-const
 --> example.jl:2:5
  |
2 |     const local factor = 2
  |     ^^^^^^^^^^^^^^^^^^^^^^ `local const` declaration is not supported

noteq-definition

Flag a method definition for != (or ). Julia defines != as const != = !(==), so it is not meant to be overloaded: define == instead, and != follows automatically.

Defining != where == should carry the method:

!=(a::Grade, b::Grade) = a.score != b.score
warning: noteq-definition
 --> example.jl:1:1
  |
1 | !=(a::Grade, b::Grade) = a.score != b.score
  | ^^ `!=` is defined as `!(==)` and should not be overloaded; define `==` instead

unused-type-parameter

Flag a where clause type parameter that is never used in the signature or body, covering bare (where T), braced (where {T, S}), bounded (where {T<:Number}), and chained (where T where S) clauses. An unread parameter is usually a refactoring leftover or a forgotten ::T annotation. Struct type parameters are exempt (phantom parameters like struct Unit{T} end are idiomatic), as are all-underscore names.

T is bound but never referenced:

function f(x) where {T}
    x + 1
end
warning: unused-type-parameter
 --> example.jl:1:22
  |
1 | function f(x) where {T}
  |                      ^ type parameter `T` is never used

missing-include-file

Flag a static include("path") whose target does not exist on disk (relative paths resolve against the including file’s directory). Running the file would throw a SystemError. Only statically resolvable includes are checked: dynamic (include(f)), interpolated (include("$dir/a.jl")), qualified (M.include(...)), and two-argument forms cannot be resolved without running the code and are never flagged.

Including a file that does not exist:

include("missing.jl")
error: missing-include-file
 --> example.jl:1:9
  |
1 | include("missing.jl")
  |         ^^^^^^^^^^^^ included file "missing.jl" does not exist

include-cycle

Flag a static include("path") whose target transitively includes this file again — including a file that includes itself. Running such a file recurses without end. Every linted file on the cycle is flagged at its own include call; a file merely included twice along different paths (a diamond) is not a cycle.

A file (here example.jl) that includes itself:

include("example.jl")
warning: include-cycle
 --> example.jl:1:9
  |
1 | include("example.jl")
  |         ^^^^^^^^^^^^ include cycle: "example.jl" transitively includes this file

duplicate-include

Flag a static include("path") that pulls in a file an earlier include in the same file already did. Julia has no include guard, so the repeat evaluates the file a second time, redefining its methods and re-running its top-level code. Paths are compared after resolution, so "a.jl" and "./a.jl" count as the same file. Including one file into two different module blocks is not flagged — that runs its definitions into two separate namespaces — and neither is a file reached twice along different include chains.

The same file included twice, so its definitions run again:

include("util.jl")
include("util.jl")
warning: duplicate-include
 --> example.jl:2:9
  |
2 | include("util.jl")
  |         ^^^^^^^^^ duplicate include: "util.jl" is already included earlier in this file

duplicate-method

Flag a method definition whose dispatch signature an earlier definition in the same file already used. Julia holds one method per signature, so the later definition silently replaces the earlier one and the earlier body becomes dead. Signatures are compared on what dispatch actually sees: the positional argument types (an unannotated argument is Any), the where specs, and any type arguments the definition applies to its own name, as a parameterized constructor does. Argument names, default values, keyword arguments, and the declared return type are not part of a method’s identity, so definitions differing only in those still collide. Definitions with different where bounds are separate methods and are not flagged, and neither are definitions inside a macro call or a conditional branch, where the written signature is not evidence of what gets defined.

The second definition replaces the first, so 1 is unreachable:

f(x::Int) = 1
f(y::Int) = 2
warning: duplicate-method
 --> example.jl:2:1
  |
2 | f(y::Int) = 2
  | ^ `f` is already defined with this signature earlier in this file; the later definition replaces the earlier one

julia-version-compat

Flag syntax newer than the project’s declared Julia support range. Fatou parses the full superset of Julia syntax, so a construct from a newer release parses cleanly even when the project targets an older version; this rule reports when a supported version predates the construct (e.g. public needs 1.11, import ... as needs 1.6). The target range is taken from --julia-version, [julia] version, or the project’s Project.toml [compat] / Manifest.toml; with no target known the rule stays silent.

Targeting Julia 1.0, but public needs 1.11 and as needs 1.6:

module M
public foo
import A as B
end
error: julia-version-compat
 --> example.jl:2:1
  |
2 | public foo
  | ^^^^^^^^^^ the `public` keyword requires Julia 1.11.0, but the project supports 1.0.0 and up
error: julia-version-compat
 --> example.jl:3:8
  |
3 | import A as B
  |        ^^^^^^ renaming with `as` in `import`/`using` requires Julia 1.6.0, but the project supports 1.0.0 and up

call-arity

Flag a call that no visible method of the function accepts: a positional count outside every method’s range, or a keyword argument no positionally-matching method declares. Such a call raises MethodError at runtime. The method table unions every tier name resolution sees — the file’s own definitions, the workspace package, and the harvested library, qualified extensions included — so an unknown method always silences the check rather than triggering it. Calls that splat arguments, carry do blocks, sit in macro calls or quoted code, or target constructors and callable values are exempt, and a file that evals or includes outside a known workspace is skipped entirely. Off by default: the rule needs project context to be sound, so the language server enables it for workspace member files, while the CLI leaves it opt-in for self-contained scripts.

half has no two-argument method:

half(x) = x / 2

half(3, 4)
warning: call-arity
 --> example.jl:3:1
  |
3 | half(3, 4)
  | ^^^^^^^^^^ no method of `half` takes 2 positional arguments (methods accept 1)

function-has-no-methods

Flag a call to a function whose every visible definition is a bodyless function f end declaration. Such a function has an empty method table, so the call raises MethodError whatever it passes. Only a name the closed world defines is checked — one belonging to this file or to the enclosing workspace package — since the owner of a using’d or Base/Core name may add methods elsewhere. A declaration the package exports or declares public is exempt as an interface hook for a package extension or a downstream package to implement, as is a name that also names a type. The file is skipped entirely when it evals, includes outside a known workspace, or usings a module the library cannot resolve, and call sites in macro calls or quoted code are exempt. Off by default: like call-arity the rule needs project context to be sound, so the language server enables it for workspace member files while the CLI leaves it opt-in.

normalize is declared but never given a method:

function normalize end

normalize("data")
warning: function-has-no-methods
 --> example.jl:3:1
  |
3 | normalize("data")
  | ^^^^^^^^^ `normalize` has no methods: every definition is a bodyless `function normalize end`

redefined-constant

Flag a write that redefines a constant name, or defines over a name that already holds a value: reassigning a const binding, assigning to a global function, type, or module name (those bind implicit constants), defining a function over a plain value, or declaring a value const after the fact. All of these error at runtime when both sites execute. A definition and a write in disjoint branches of the same if are exempt — only one branch runs. Adding a method to a function and defining an outer constructor on a type are legal and stay silent. No fix: the rule cannot know which of the two definitions the author meant to keep.

Reassigning a const binding errors at runtime:

const threshold = 1.0
threshold = 2.0
warning: redefined-constant
 --> example.jl:2:1
  |
2 | threshold = 2.0
  | ^^^^^^^^^ reassignment of constant `threshold`

count already holds a value, so the method definition fails:

count = 0
count(xs) = length(xs)
warning: redefined-constant
 --> example.jl:2:1
  |
2 | count(xs) = length(xs)
  | ^^^^^ cannot define function `count`: it already has a value

type-piracy

Flag a method definition that extends a function the current module does not own, using only argument types it does not own either (“type piracy”). Because Julia dispatches on one global method table, such a method silently changes behavior for every other user of those types the moment the module loads. A definition is fine as long as it owns the function or at least one argument type (a type parameter or where bound counts). The rule is sound-first: it flags only when it can prove the function and every readable argument type are foreign, withholding on anything unknown, and it skips the whole file when a whole-module using cannot be resolved. Off by default: it needs project context, so the language server enables it for workspace member files while the CLI leaves it opt-in via --select.

Extending Base.show for only Base types is piracy:

Base.show(io::IO, x::Int) = print(io, x)
warning: type-piracy
 --> example.jl:1:6
  |
1 | Base.show(io::IO, x::Int) = print(io, x)
  |      ^^^^ `Base.show` commits type piracy: it extends a function this module does not own, and no argument type is owned here either

unreachable-code

Flag a statement no path of execution can reach: the tail after an unconditional return, throw, error, or rethrow, after an if/else that diverges in every arm, or after a while true with no break. The code runs, but the flagged statement never does, so it is either dead weight or a sign that the divergence above it is misplaced. Reachability comes from the file’s control-flow graph, so a for that may run zero times, an if with no else, a catch clause, and a conditional a && return all keep their tails live. No fix is offered: deleting the statement is a judgment call, and keeping it may be the point when the divergence is the bug.

Nothing after an unconditional return can run:

function f(x)
    return x + 1
    println("never")
end
warning: unreachable-code
 --> example.jl:3:5
  |
3 |     println("never")
  |     ^^^^^^^^^^^^^^^^ unreachable code: no path of execution reaches this statement

Both arms diverge, so the tail is dead too:

function classify(x)
    if x > 0
        return :pos
    else
        throw(DomainError(x))
    end
    return :unknown
end
warning: unreachable-code
 --> example.jl:7:5
  |
7 |     return :unknown
  |     ^^^^^^^^^^^^^^^ unreachable code: no path of execution reaches this statement

unresolved-import

Flag a using/import of a module the enclosing project cannot load: a name that is neither in its Project.toml [deps] nor provided by the harvested environment (the standard library included). Julia resolves a bare using Foo against the active project, so an undeclared name raises ArgumentError: Package Foo not found in current path when the module loads. Relative paths (using .Sub), interpolated paths, and loads inside quoted code or macro calls are exempt, and a name the harvest resolved is always accepted — so a transitive dependency goes unreported rather than risking a false positive. Off by default: the rule needs the project context that only a package source file carries, so the language server enables it for workspace member files while the CLI leaves it opt-in.

The project declares LinearAlgebra in [deps], but not Frobnicate:

using LinearAlgebra
using Frobnicate
warning: unresolved-import
 --> example.jl:2:7
  |
2 | using Frobnicate
  |       ^^^^^^^^^^ `Frobnicate` is not a dependency of this project

kwarg-default-mismatch

Flag a keyword parameter whose literal default cannot be an instance of its declared type, as in g(; y::Int = 1.0). A keyword’s ::T is not an implicit convert: Julia lowers it into a dispatch constraint on the inner method the default is passed to, so g() raises a MethodError every time. The check is exact, like dispatch itself — y::Float64 = 1 and y::Int8 = 1 are mismatches too — and fires only when both sides are certain: a bare, concrete Core type that resolves to Base, and a default whose own spelling pins its type down. Abstract and parametric annotations (Real, Vector{Int}), computed defaults, and the literals whose type follows their digit count (0x01) are all left alone.

y is declared Int, so the Float64 default never matches it:

function scale(xs; y::Int = 1.0)
    xs .* y
end
error: kwarg-default-mismatch
 --> example.jl:1:20
  |
1 | function scale(xs; y::Int = 1.0)
  |                    ^^^^^^^^^^^^ keyword argument `y` is declared `::Int`, but its default `1.0` has type `Float64`
  = help: a keyword's `::T` is a dispatch constraint, not a `convert`, so the default raises a `MethodError`

Julia does not promote here either — the default has to be an Int8 already:

counter(; start::Int8 = 0) = start
error: kwarg-default-mismatch
 --> example.jl:1:11
  |
1 | counter(; start::Int8 = 0) = start
  |           ^^^^^^^^^^^^^^^ keyword argument `start` is declared `::Int8`, but its default `0` has type `Int`
  = help: a keyword's `::T` is a dispatch constraint, not a `convert`, so the default raises a `MethodError`

invalid-type-declaration

Flag a :: type declaration whose declared type is a function. The right side of a :: must be a type, and Julia checks it eagerly: f(x::g) for a generic function g raises a TypeError when the method is defined, and a value-position x::g raises one as soon as it runs. The rule is deliberately partial, since only a closed answer about a name is trustworthy: it fires for a function defined in this file and for a top-level function of the enclosing workspace package, and stays silent for a Base/Core name (the export snapshot records no kinds), an imported name, and any binding that could hold a type such as a variable or a const alias. A same-named type, const, or module anywhere in the file or the package withholds the finding, so an outer constructor (Foo(x::Int) = …, which binds Foo as a function too) is never flagged. The file is skipped entirely when it evals, includes outside a known workspace, or usings a module the library cannot resolve, and annotations inside a macro call or quoted code are exempt. Off by default: like the other resolution-gated rules it needs project context, so the language server enables it for workspace member files while the CLI leaves it opt-in via --select.

scale is a function, so declaring y::scale is a TypeError:

scale(x) = 2x

apply(y::scale) = y
warning: invalid-type-declaration
 --> example.jl:3:10
  |
3 | apply(y::scale) = y
  |          ^^^^^ `scale` is a function, not a type: `::scale` is not a valid type declaration

assignment-in-condition

Flag a bare = assignment used as the test of an if/elseif/while. It is valid Julia but almost always a typo for ==, so it is reported with a safe fix that rewrites = to ==.

= where == was meant:

if x = 5
    println(x)
end
warning: assignment-in-condition
 --> example.jl:1:4
  |
1 | if x = 5
  |    ^^^^^ assignment used as a condition; did you mean `==`?
  = help: Replace `=` with `==` (safe fix)

After applying the fix:

if x == 5
    println(x)
end

nothing-comparison

Flag x == nothing / x != nothing, which compares against nothing by value. nothing is the singleton instance of Nothing, so an identity test (=== / !==, or isnothing) is meant: it is faster and cannot be overloaded. The rule reports a safe fix rewriting == to === and != to !==.

Comparing against nothing by value:

if x == nothing
    1
end
warning: nothing-comparison
 --> example.jl:1:4
  |
1 | if x == nothing
  |    ^^^^^^^^^^^^ comparison against `nothing` by value; use `===` or `isnothing`
  = help: Replace `==` with `===` (safe fix)

After applying the fix:

if x === nothing
    1
end

missing-comparison

Flag x == missing / x != missing. missing propagates through ==, so the comparison is always missing no matter what x is, and using it as a condition raises a TypeError. Use ismissing (or the identity test === / !==) instead. The rule reports an unsafe fix rewriting == to === and != to !==: the rewrite turns a missing result into a Bool, which is the intent but is still a change in behavior.

Comparing against missing by value:

if x == missing
    1
end
warning: missing-comparison
 --> example.jl:1:4
  |
1 | if x == missing
  |    ^^^^^^^^^^^^ comparison against `missing` by value is always `missing`; use `ismissing` or `===`
  = help: Replace `==` with `===` (unsafe fix, requires `--unsafe-fixes`)

constant-condition

Flag a true/false literal used as an if/elseif/while test or as an operand of the short-circuit &&/||. The branch or short-circuit is decided before the code runs, so the literal is usually leftover debugging or a mistyped name. while true is exempt as Julia’s idiomatic infinite loop. No fix: removing the constant means restructuring the branch.

A literal if test always takes the branch:

if true
    println("always")
end
warning: constant-condition
 --> example.jl:1:4
  |
1 | if true
  |    ^^^^ this condition is always `true`

A literal operand decides && at parse time:

ok = false && check(x)
warning: constant-condition
 --> example.jl:1:6
  |
1 | ok = false && check(x)
  |      ^^^^^ `&&` has a constant `false` operand

module-shadows-parent

Flag a nested module with the same name as its direct parent module. A module binds its own name inside itself, so the child rebinds that self-reference: A in the parent body then refers to the child, and qualified names like A.x resolve against the wrong module. The usual cause is a file included into the module it already defines. No fix: renaming a module means updating every reference to it.

A submodule shadowing the module that contains it:

module A

module A
end

end
warning: module-shadows-parent
 --> example.jl:3:8
  |
3 | module A
  |        ^ module `A` has the same name as its parent module

loop-variable-shadow

Flag a for loop whose index variable is already an enclosing for loop’s index, and an assignment to a loop variable inside its own loop. The nested for binds a fresh variable, so the outer index is unreachable inside it — usually a copy-pasted inner loop whose index was never renamed. An assignment to a loop variable is discarded at the next iteration, since for rebinds the variable from the iterator on every pass, so it can never steer the iteration. Comprehension and generator clauses are left alone, as is reuse across a function body, a closure, or a do block. No fix: renaming an index or dropping an assignment changes what the body computes.

A nested loop reusing the enclosing loop’s index:

for i in 1:3
    for i in 1:2
        println(i)
    end
end
warning: loop-variable-shadow
 --> example.jl:2:9
  |
2 |     for i in 1:2
  |         ^ loop variable `i` shadows the enclosing loop's variable

An assignment the next iteration discards:

for i in 1:10
    if isodd(i)
        i += 1
    end
    println(i)
end
warning: loop-variable-shadow
 --> example.jl:3:9
  |
3 |         i += 1
  |         ^ assignment to loop variable `i` is discarded at the next iteration

index-from-length

Flag two suspect for-loop iteration specs. First, for i in 1:length(x) (or 1:size(x, d)) where the loop variable then indexes x: prefer eachindex(x) (or axes(x, d)), which stays correct for collections whose indices are not one-based. The match is name-based — any length/size call counts — and, lacking type information to exempt collections that really are one-based like Vector, the rule is opinionated, so it only fires when the loop variable actually indexes the collection. The first shape carries an unsafe fix rewriting the 1:length/1:size prefix to eachindex/axes: the rewrite is only value-equivalent when the collection’s indices are one-based and dense, which cannot be proven without type information, so it needs --unsafe-fixes. Second, for i in 3.5: iterating a bare numeric literal runs the loop body once and is almost always a mistaken range; no fix, since the intended range is unknowable.

1:length(x) used to index x:

for i in 1:length(x)
    println(x[i])
end
warning: index-from-length
 --> example.jl:1:10
  |
1 | for i in 1:length(x)
  |          ^^^^^^^^^^^ iterate `eachindex(x)` instead of `1:length(x)`
  = help: Replace `1:length` with `eachindex` (unsafe fix, requires `--unsafe-fixes`)

Iterating a bare number loops once:

for i in 3.5
    println(i)
end
warning: index-from-length
 --> example.jl:1:10
  |
1 | for i in 3.5
  |          ^^^ iterating a numeric literal runs the loop body once; did you mean a range?

discouraged-function

Flag a call to a function on a configurable deny-list. The built-in set covers Base functions with process-wide or memory-unsafe effects — exit, cd, redirect_stdout, redirect_stderr, and the unsafe_*/pointer conversions — each reported with the alternative to reach for.

Configure it under [lint.rules.discouraged-function]: functions replaces the built-in set, extend-functions adds to it (an entry there also rewords a built-in), and functions = {} silences the rule without ignoring it. Both are tables mapping a function name to the suggestion shown in the diagnostic.

A call carrying a do block is never reported, since for cd and the redirect_* functions that form is the recommended alternative. A qualified callee (Base.exit) is a different name and does not match. A built-in name is only reported once it is confirmed to be Base’s, so a local of the same name — or a file whose imports cannot be resolved — reports nothing; a name the project configured is reported unless a definition in the same file shadows it. No fix is offered, since the rewrite is a judgment call.

exit ends the process and cd leaves the working directory changed:

function cleanup()
    cd("/tmp")
    exit(1)
end
warning: discouraged-function
 --> example.jl:2:5
  |
2 |     cd("/tmp")
  |     ^^ `cd` is discouraged: use the `cd(f, dir)` do-block form so the working directory is restored
warning: discouraged-function
 --> example.jl:3:5
  |
3 |     exit(1)
  |     ^^^^ `exit` is discouraged: let the caller decide when the process ends

typeof-comparison

Flag typeof(x) == T / typeof(x) != T (in either operand order), which tests for the exact type and so answers false for every subtype of Ttypeof(x) == Integer is false for every integer, since typeof returns the concrete Int64. Use x isa T, the subtype test.

The already-correct === / !==, the broadcast .== / .!=, and a comparison chain are left alone, as is typeof(a) == typeof(b), which compares two runtime types and has no isa spelling. The callee must be confirmed to be Base’s typeof, so a local shadow, a qualified Base.typeof, or a file whose imports cannot be resolved reports nothing.

The rule reports an unsafe fix rewriting the comparison to x isa T (!(x isa T) for !=): widening the exact-type test to the subtype tree is the intent, but it is still a change in behavior, and a caller may genuinely want exact-type identity. The fix is withheld — the finding still stands — when a comment sits inside the rewritten span, or when typeof’s argument binds more loosely than isa and so cannot be spliced in without parentheses.

typeof(x) == Integer is false for every integer:

if typeof(x) == Integer
    1
end
warning: typeof-comparison
 --> example.jl:1:4
  |
1 | if typeof(x) == Integer
  |    ^^^^^^^^^^^^^^^^^^^^ `typeof(x) == T` matches only the exact type; use `x isa T`, which includes subtypes
  = help: Rewrite as an `isa` test (unsafe fix, requires `--unsafe-fixes`)

shadowed-base-name

Flag a call to a name the file binds to a value while Base or Core exports it as a function. Julia has one namespace and no call-position fallback, so the binding masks the Base name everywhere: after length = 3, a later length(xs) raises a MethodError for calling an Int, and assigning the other way round fails too once the module has used the Base name. Both a binding and a call are required — the binding alone is ordinary Julia. Only bindings that plainly hold a value count. Method, macro, type, and module definitions of the name are exempt, as is import Base: length; so is a parameter, since passing a function in under a Base name (request(stack::Base.Callable, …)) is the higher-order idiom rather than an accident, though a catch variable — which holds the thrown exception — still counts. A binding the source visibly assigns something callable (a lambda, another builtin under a new name, or any qualified path such as exit = t.__exit__) is exempt too, as is anything defined or called inside quoted code or a macro call, where a DSL may spell an attribute length = 32 without touching Base. No fix: renaming the binding means rewriting every reference to it.

The assignment masks Base’s length, so the call fails:

length = 3
n = length(xs)
warning: shadowed-base-name
 --> example.jl:2:5
  |
2 | n = length(xs)
  |     ^^^^^^ `length` here is this file's own binding, not Base's `length`

A catch variable can mask a builtin just as well:

try
    risky()
catch error
    error("failed")
end
warning: shadowed-base-name
 --> example.jl:4:5
  |
4 |     error("failed")
  |     ^^^^^ `error` here is this file's own binding, not Base's `error`

non-public-access

Flag a qualified read of a name the target module neither exports nor declares public. Those two statements are how a Julia module says what its API is — public (1.11) declares intent without attaching the name to a using — so everything else is internal and free to change in a patch release. Only a qualifier that really names a module is checked: a local, a parameter, a name an item list bound (import Foo: Bar), and any plain field access are values, not modules. Two kinds of module are left alone: one that declares no public API at all, whose whole surface is reached qualified by design, and one that re-exports with Reexport.jl, whose @reexport using Bar is a macro call the index cannot follow. So is the package under development, whose own internals its files may use. A Base/Core export reaches through any module, since every module but a baremodule implicitly does using BaseThreads.ReentrantLock is Base’s type — and so do the eval and include every module binds for itself. Definition sites are not reads: Base.show(io, x) = … extends a function and Foo.bar = 1 writes one, and code inside a quote or a macro call is exempt as everywhere else. Off by default: the rule needs the harvested library that only project context provides, and it reports what a module declared, so a package whose documented interface is qualified and unexported is reported too. No fix — the caller cannot make someone else’s name public.

summarysize is part of Base’s public API; unwrap_unionall is not:

Base.summarysize(x)
Base.unwrap_unionall(T)
warning: non-public-access
 --> example.jl:2:1
  |
2 | Base.unwrap_unionall(T)
  | ^^^^^^^^^^^^^^^^^^^^ `Base` does not export `unwrap_unionall` or declare it `public`

Macros are checked in their own namespace:

Base.@_inline_meta
warning: non-public-access
 --> example.jl:1:1
  |
1 | Base.@_inline_meta
  | ^^^^^^^^^^^^^^^^^^ `Base` does not export `@_inline_meta` or declare it `public`

eager-broadcast

Flag a Base reducer applied to a broadcast that exists only to be reduced: sum(abs.(xs)) builds a whole mapped array and then sums it away, where sum(abs, xs) applies abs as it reduces and allocates nothing. The reducers with such an (f, itr) method are all, any, count, maximum, minimum, prod, and sum.

Only the exact reducer(f.(x)) shape is flagged. A keyword argument on either call, a second positional argument, and a fused multi-container broadcast (hypot.(xs, ys)) all describe a different call and are left alone, as is a broadcast operator (any(xs .> 0)), which names no function to pass. The reducer must be confirmed to be Base’s, so a local shadow, a qualified Base.sum, or a file whose imports cannot be resolved reports nothing.

The fix moves the function into the reducer’s first argument, but needs --unsafe-fixes: broadcasting treats a scalar as a zero-dimensional container of itself where the two-argument method iterates it, and any/all stop at the first decisive element, so a function that prints, mutates, or throws runs a different number of times. The fix is withheld — the finding still stands — when a comment sits in the rewritten span.

The mapped array is built only to be summed away:

total = sum(abs.(residuals))
warning: eager-broadcast
 --> example.jl:1:9
  |
1 | total = sum(abs.(residuals))
  |         ^^^^^^^^^^^^^^^^^^^^ call `sum(abs, residuals)` instead of `sum(abs.(residuals))`, which materializes the broadcast result
  = help: Pass the function as `sum`'s first argument (unsafe fix, requires `--unsafe-fixes`)

any need not test every element:

if any(isnan.(xs))
    error("bad data")
end
warning: eager-broadcast
 --> example.jl:1:4
  |
1 | if any(isnan.(xs))
  |    ^^^^^^^^^^^^^^^ call `any(isnan, xs)` instead of `any(isnan.(xs))`, which materializes the broadcast result
  = help: Pass the function as `any`'s first argument (unsafe fix, requires `--unsafe-fixes`)

sorted-extremum

Flag indexing one end of a freshly sorted collection: sort(xs)[1] copies and orders every element to answer what minimum(xs) answers in one pass, and sort(xs)[end] (like sort(xs)[begin] for the other end) does the same for maximum(xs).

Only a plain sort(x) at either end is flagged. A rev, by, or lt keyword decides which element lands where, so the index no longer picks the extremum a reducer would return; any other index (sort(xs)[2]) genuinely needs the order; and sort! is a different function. sort must be confirmed to be Base’s, so a local shadow or a qualified Base.sort reports nothing.

The fix replaces the indexing with the extremum call, but needs --unsafe-fixes: sorting orders NaN last, so sort(xs)[1] returns the smallest real number where minimum(xs) propagates the NaN. It is withheld — the finding still stands — when the file’s minimum/maximum is not Base’s, or when a comment sits in the rewritten span outside the collection.

Sorting to read the smallest element:

lowest = sort(scores)[1]
warning: sorted-extremum
 --> example.jl:1:10
  |
1 | lowest = sort(scores)[1]
  |          ^^^^^^^^^^^^^^^ call `minimum(scores)` instead of `sort(scores)[1]`, which sorts the whole collection
  = help: Replace the sorted indexing with `minimum` (unsafe fix, requires `--unsafe-fixes`)

And the largest:

highest = sort(scores)[end]
warning: sorted-extremum
 --> example.jl:1:11
  |
1 | highest = sort(scores)[end]
  |           ^^^^^^^^^^^^^^^^^ call `maximum(scores)` instead of `sort(scores)[end]`, which sorts the whole collection
  = help: Replace the sorted indexing with `maximum` (unsafe fix, requires `--unsafe-fixes`)

length-findall

Flag length(findall(p, x)), which allocates a vector of every matching index just to ask how many there are. count(p, x) answers with a counter and no allocation, and covers the one-argument form too: length(findall(mask)) is count(mask).

Both calls must be plain — no keyword arguments, no splats — and both length and findall must be confirmed to be Base’s, so a local shadow, a qualified Base.findall, or a file whose imports cannot be resolved reports nothing.

The fix rewrites the pair to a count call carrying findall’s own argument list, but needs --unsafe-fixes: findall walks a collection’s keys where count iterates its elements, which is the same walk for an array and a different one for a Dict, whose values findall tests and whose key => value pairs count does. It is withheld — the finding still stands — when the file’s count is not Base’s, or when a comment sits in the rewritten span outside the argument list.

The index vector is built only to be measured:

n = length(findall(isodd, xs))
warning: length-findall
 --> example.jl:1:5
  |
1 | n = length(findall(isodd, xs))
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^ call `count(isodd, xs)` instead of `length(findall(isodd, xs))`, which builds an index vector just to count it
  = help: Count the matches directly with `count` (unsafe fix, requires `--unsafe-fixes`)

The one-argument form counts a mask:

hits = length(findall(mask))
warning: length-findall
 --> example.jl:1:8
  |
1 | hits = length(findall(mask))
  |        ^^^^^^^^^^^^^^^^^^^^^ call `count(mask)` instead of `length(findall(mask))`, which builds an index vector just to count it
  = help: Count the matches directly with `count` (unsafe fix, requires `--unsafe-fixes`)

fixed-regex

Flag occursin(r"abc", s), whose regex literal carries no metacharacter and so matches one fixed substring. occursin("abc", s) asks the same question of the same values and answers it with a substring search instead of the regex engine.

Every Base function that takes a pattern is read the same way, each at the argument that holds it: occursin(r"abc", s), the flipped contains(s, r"abc"), startswith/endswith, split/eachsplit, each => pair of a replace, and the curried forms that fix the pattern (contains(r"abc")). The curried occursin(s) fixes the haystack instead, so its argument is no pattern and is left alone, and rsplit takes no regex at all.

The pattern must be a plain r"..." — no flag suffix, since a flag changes what the pattern means, and no interpolation — and it must be non-empty and free of every PCRE metacharacter and backslash escape. The callee must be confirmed to be Base’s, so a local shadow, a qualified Base.occursin, or a file whose imports cannot be resolved reports nothing.

The safe fix deletes the r prefix and nothing else. A pattern with no backslash and no $ reads identically as an ordinary string literal, and the literal keeps its own delimiters, so the string it spells is unchanged.

The pattern is a plain substring, matched through PCRE:

if occursin(r"error", line)
    push!(failures, line)
end
warning: fixed-regex
 --> example.jl:1:13
  |
1 | if occursin(r"error", line)
  |             ^^^^^^^^ use the plain string `"error"` instead of the regex `r"error"`, whose pattern has no metacharacter
  = help: Drop the `r` prefix and match the substring (safe fix)

After applying the fix:

if occursin("error", line)
    push!(failures, line)
end

The same search written the other way round, curried:

failures = filter(contains(r"error"), lines)
warning: fixed-regex
 --> example.jl:1:28
  |
1 | failures = filter(contains(r"error"), lines)
  |                            ^^^^^^^^ use the plain string `"error"` instead of the regex `r"error"`, whose pattern has no metacharacter
  = help: Drop the `r` prefix and match the substring (safe fix)

After applying the fix:

failures = filter(contains("error"), lines)

Every other pattern position reads the same way:

fields = split(line, r"::")
warning: fixed-regex
 --> example.jl:1:22
  |
1 | fields = split(line, r"::")
  |                      ^^^^^ use the plain string `"::"` instead of the regex `r"::"`, whose pattern has no metacharacter
  = help: Drop the `r` prefix and match the substring (safe fix)

After applying the fix:

fields = split(line, "::")

comparison-negation

Flag ! applied to a parenthesized equality test, which Julia spells with a single operator: !(a == b) is a != b, !(a === b) is a !== b, and both read back the other way. The Unicode spellings , , and collapse the same way.

The rewrite is exact rather than merely equivalent-in-practice: Base defines !=(x, y) = !(x == y) and !==(x, y) = !(x === y), so the two spellings agree on every input by construction.

Only the equality family is reported. The orderings are left alone because < and >= are independent methods rather than negations of each other, and they disagree on exactly the inputs a partial order is partial about: !(NaN < 1) is true while NaN >= 1 is false. The broadcast forms a .== b and .!x are containers of values rather than tests, and a comparison chain (a == b == c) has no two-operand rewrite, so none of them is flagged.

The rule reports a safe fix that reuses the comparison’s own source text with the operator swapped, so spacing and any comment between the operands survive. The fix is withheld — the finding still stands — when a comment sits in the deleted !( or ), and when the negation sits somewhere a bare comparison would rebind, as in x + !(a == b).

Negating an equality test spells the inequality:

if !(status == :ok)
    retry()
end
warning: comparison-negation
 --> example.jl:1:4
  |
1 | if !(status == :ok)
  |    ^^^^^^^^^^^^^^^^ write the comparison directly: `!(a == b)` is `a != b`
  = help: Rewrite as `!=` (safe fix)

After applying the fix:

if status != :ok
    retry()
end

The identity comparison negates the same way:

found = !(lookup(key) === nothing)
warning: comparison-negation
 --> example.jl:1:9
  |
1 | found = !(lookup(key) === nothing)
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^ write the comparison directly: `!(a === b)` is `a !== b`
  = help: Rewrite as `!==` (safe fix)

After applying the fix:

found = lookup(key) !== nothing

length-zero

Flag a comparison of length(x) against 0 or 1 that asks whether x is empty. A length is a non-negative integer, so length(x) == 0, length(x) <= 0, and length(x) < 1 all spell isempty(x), while length(x) != 0, length(x) > 0, and length(x) >= 1 spell !isempty(x); each mirrored spelling with the literal first (0 < length(x)) collapses the same way. isempty is the test collections actually implement, and it need not count what it throws away.

The already-deliberate ===, the broadcast .== / .<, and a comparison chain are left alone, as are the bounds that ask a different question (length(x) == 1) or none at all (length(x) >= 0, which is always true). The callee must be confirmed to be Base’s length, so a local shadow, a qualified Base.length, or a file whose imports cannot be resolved reports nothing.

The rule reports a safe fix replacing the comparison with isempty(x) or !isempty(x), reusing the argument’s own source text. The fix is withheld — the finding still stands — when the file’s isempty is not Base’s, or when a comment sits in the rewritten span outside the argument.

Comparing a length against zero is an emptiness test:

if length(xs) == 0
    println("nothing to do")
end
warning: length-zero
 --> example.jl:1:4
  |
1 | if length(xs) == 0
  |    ^^^^^^^^^^^^^^^ test `isempty(xs)` instead of comparing `length(xs)` to `0`
  = help: Rewrite as an `isempty` test (safe fix)

After applying the fix:

if isempty(xs)
    println("nothing to do")
end

The non-emptiness spellings, including the mirrored ones:

while 0 < length(queue)
    pop!(queue)
end
warning: length-zero
 --> example.jl:1:7
  |
1 | while 0 < length(queue)
  |       ^^^^^^^^^^^^^^^^^ test `!isempty(queue)` instead of comparing `length(queue)` to `0`
  = help: Rewrite as an `isempty` test (safe fix)

After applying the fix:

while !isempty(queue)
    pop!(queue)
end

redundant-boolean

Flag a test compared against a boolean literal, or a conditional whose two arms are the literals themselves. x == true and x != false are x; x == false and x != true are !x; c ? true : false is c and c ? false : true is !c. Because == and != are symmetric, the mirrored spellings (true == x) collapse the same way.

This is distinct from constant-condition, which owns the literal-as-test case (if true), where the branch is decided before the code runs.

The two halves do not ship the same fix. The conditional rewrite is reported as a safe fix: ?: requires a Bool test, so on every input that does not throw, c ? true : false hands back that very Bool. The comparison rewrite is reported as an unsafe fix, because == is not identity — it promotes across the numeric tower (1 == true is true), answers missing for missing, and is overloadable — so the two spellings agree only when the operand is already a Bool.

The deliberate === / !==, the broadcast .== / .!=, and a comparison chain are left alone, as are a comparison of two boolean literals (no operand survives it) and a conditional whose arms agree (constant rather than redundant).

The fix reuses the surviving operand’s own source text, parenthesizing it when a bare ! would rebind (a + b == false becomes !(a + b)). It is withheld — the finding still stands — when a comment sits in the replaced span outside that operand.

A conditional over the two literals is its own test:

ready = queue_started(q) ? true : false
warning: redundant-boolean
 --> example.jl:1:9
  |
1 | ready = queue_started(q) ? true : false
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this conditional just yields `queue_started(q)`
  = help: Replace the conditional with its test (safe fix)

After applying the fix:

ready = queue_started(q)

Comparing a test to a boolean literal restates it:

if x.valid == false
    reject(x)
end
warning: redundant-boolean
 --> example.jl:1:4
  |
1 | if x.valid == false
  |    ^^^^^^^^^^^^^^^^ comparing to `false` is redundant: write `!x.valid`
  = help: Drop the comparison to `false` (unsafe fix, requires `--unsafe-fixes`)

string-boundary

Flag occursin(r"^abc", s) and occursin(r"abc$", s), boundary tests written as anchored regex searches. startswith(s, "abc") and endswith(s, "abc") name the boundary they test and need no match engine.

The pattern must be a plain r"..." — no flag suffix, since i and m change what an anchor means — anchored at exactly one end with a non-empty, metacharacter-free remainder; r"^abc$" is an exact match rather than a boundary test. The callee must be confirmed to be Base’s, and the needle is read wherever its spelling puts it: occursin(r"^abc", s), the flipped contains(s, r"^abc"), and the curried contains(r"^abc"), which rewrites to the curried startswith("abc").

The startswith fix is safe: ^ matches at the start of the subject and nowhere else. The endswith fix needs --unsafe-fixes, because PCRE’s $ also matches before a final newline — occursin(r"abc$", "abc\n") is true where endswith("abc\n", "abc") is false. Either fix is withheld — the finding still stands — when the file’s startswith/endswith is not Base’s, or when a comment sits in the rewritten span outside the haystack and the pattern.

A leading anchor is a prefix test:

if occursin(r"^Test", name)
    run(name)
end
warning: string-boundary
 --> example.jl:1:4
  |
1 | if occursin(r"^Test", name)
  |    ^^^^^^^^^^^^^^^^^^^^^^^^ test `startswith(name, "Test")` instead of matching the anchored regex `r"^Test"`
  = help: Test the prefix with `startswith` (safe fix)

After applying the fix:

if startswith(name, "Test")
    run(name)
end

A trailing anchor is a suffix test:

sources = filter(f -> occursin(r"_test$", f), files)
warning: string-boundary
 --> example.jl:1:23
  |
1 | sources = filter(f -> occursin(r"_test$", f), files)
  |                       ^^^^^^^^^^^^^^^^^^^^^^ test `endswith(f, "_test")` instead of matching the anchored regex `r"_test$"`
  = help: Test the suffix with `endswith`, which does not match before a trailing newline (unsafe fix, requires `--unsafe-fixes`)

A curried contains rewrites to a curried predicate:

sources = filter(contains(r"^src"), files)
warning: string-boundary
 --> example.jl:1:18
  |
1 | sources = filter(contains(r"^src"), files)
  |                  ^^^^^^^^^^^^^^^^^ test `startswith("src")` instead of matching the anchored regex `r"^src"`
  = help: Test the prefix with `startswith` (safe fix)

After applying the fix:

sources = filter(startswith("src"), files)

unnecessary-nesting

Flag an if whose entire body is another if, where neither carries an elseif or an else. The two tests are one && test spread over two levels of indentation: if a; if b; body; end; end is if a && b; body; end.

The two spellings agree on every input. if demands a Bool and && hands its right operand back untouched, so the merged test stops on a false a exactly where the nested form skips the inner if, and if opens no scope in Julia, so merging the blocks rebinds nothing.

An alternative on either if breaks that agreement and is not reported: with an outer else, the case where a holds and b does not runs nothing before the merge and the else branch after it. A body holding anything besides the inner if is left alone for the same reason — the outer test guards more than the inner one.

The fix splices the two tests and the inner block verbatim, parenthesizing a test that binds looser than && (if a || c nested in if b becomes (a || c) && b). It is withheld — the finding still stands — when a comment sits in the discarded headers. The inner body keeps its indentation, which the formatter settles.

An if guarding nothing but another if is one test:

if isopen(io)
    if !eof(io)
        read(io)
    end
end
warning: unnecessary-nesting
 --> example.jl:1:1
  |
1 | if isopen(io)
  | ^^^^^^^^^^^^^ this `if` only guards another `if`: write `if isopen(io) && !eof(io)`
  = help: Merge the nested `if` into its parent with `&&` (safe fix)

After applying the fix:

if isopen(io) && !eof(io)
        read(io)
    end

misnamed-suppression

Flag a # fatou-ignore directive that names a rule the linter does not ship. Suppression matches a rule by its exact ID, so a misspelled, renamed, or foreign ID silences nothing while reading as though it does. When exactly one shipped rule is a near match, the finding carries a safe fix that rewrites the ID and leaves the reason untouched.

A directive naming a rule that does not exist:

# fatou-ignore unused-bindings: set up by the C library
function f()
    handle = open_device()
    1
end
warning: misnamed-suppression
 --> example.jl:1:16
  |
1 | # fatou-ignore unused-bindings: set up by the C library
  |                ^^^^^^^^^^^^^^^ `unused-bindings` is not a fatou rule; this suppresses nothing
  = help: Replace with `unused-binding` (safe fix)

After applying the fix:

# fatou-ignore unused-binding: set up by the C library
function f()
    handle = open_device()
    1
end

blanket-suppression

Flag a # fatou-ignore directive that names no rule. A bare # fatou-ignore-file silences every rule in the file, including rules added later; a bare # fatou-ignore silences nothing at all, because the node-level form requires a rule ID. Name the rule the directive is meant to suppress. There is no fix: nothing in the source says which rule that is.

A file-wide directive naming no rule turns the linter off for the whole file:

# fatou-ignore-file: generated code

struct Point
    x::Float64
end
warning: blanket-suppression
 --> example.jl:1:1
  |
1 | # fatou-ignore-file: generated code
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ suppression names no rule; it silences every rule in this file
  = help: name the rule: `# fatou-ignore <rule>: <reason>`

unexplained-suppression

Flag a # fatou-ignore directive that states no reason. The : <reason> part is optional, so this rule is off by default; select it in a project that wants every suppression to record why the finding was accepted. There is no fix: the reason is the part only the author can supply.

A suppression with nothing to say for itself:

# fatou-ignore unused-binding
function f()
    handle = open_device()
    1
end
warning: unexplained-suppression
 --> example.jl:1:1
  |
1 | # fatou-ignore unused-binding
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ suppression states no reason
  = help: add one: `# fatou-ignore <rule>: <reason>`

outdated-suppression

Flag a # fatou-ignore directive that suppressed nothing: either the rule it names ran and reported nothing it covers, or the directive has no code after it to apply to. A rule that this run did not enable, and any rule in a file whose names cannot be resolved (one that evals, or usings a module the run did not harvest), is dormant rather than stale and is never reported. The safe fix deletes the directive.

A directive at the end of a file, with nothing left to suppress:

function f(x)
    x + 1
end
# fatou-ignore unused-binding: the scratch value below
warning: outdated-suppression
 --> example.jl:4:1
  |
4 | # fatou-ignore unused-binding: the scratch value below
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ suppression has nothing after it to apply to
  = help: delete the directive
  = help: Delete the suppression (safe fix)

After applying the fix:

function f(x)
    x + 1
end