Unused variable Linter #
This file implements the unused variable linter, which runs automatically on all commands and reports any local variables that are never referred to, using information from the info tree.
It is not immediately obvious but this is a surprisingly expensive check without
some optimizations. The main complication is that it can be difficult to
determine what constitutes a "use" apart from direct references to a variable
that we can easily find in the info tree. For example, we would like this to be
considered a use of x
:
def foo (x : Nat) : Nat := by assumption
The final proof term is fun x => x
so clearly x
was used, but we can't make
use of this because the final proof term is after we have abstracted over the
original fvar
for x
. Instead, we make sure to store the proof term before
abstraction but after instantiation of mvars in the info tree and retrieve it in
the linter. Using the instantiated term is very important as redoing that step
in the linter can be prohibitively expensive. The downside of special-casing the
definition body in this way is that while it works for parameters, it does not
work for local variables in the body, so we ignore them by default if any tactic
infos are present (linter.unusedVariables.analyzeTactics
).
If we do turn on this option and look further into the tactic state, we can see
the fvar
show up in the instantiation to the original goal metavariable
?m : Nat := x
, but it is not always the case that we can follow metavariable
instantiations to determine what happened after the fact, because tactics might
skip the goal metavariable and instantiate some other metavariable created prior
to it instead. Instead, we use a (much more expensive) overapproximation, which
is just to look through the entire metavariable context looking for occurrences
of x
. We use caching to ensure that this is still linear in the size of the
info tree, even though there are many metavariable contexts in all the
intermediate stages of elaboration; these are highly similar and make use of
PersistentHashMap
so there is a lot of subterm sharing we can take advantage
of.
The @[unused_variables_ignore_fn]
attribute #
Some occurrences of variables are deliberately unused, or at least we don't want to lint on unused variables in these positions. For example:
def foo (x : Nat) : (y : Nat) → Nat := fun _ => x
-- ^ don't lint this unused variable because it is public API
They are generally a syntactic criterion, so we allow adding custom
IgnoreFunction
s so that external syntax can also opt in to lint suppression,
like so:
macro (name := foobarKind) "foobar " name:ident : command => `(def foo ($name : Nat) := 0)
foobar n -- linted because n is unused in the macro expansion
@[unused_variables_ignore_fn]
def ignoreFoobar : Lean.Linter.IgnoreFunction := fun _ stack _ => stack.matches [``foobarKind]
foobar n -- not linted
Enables or disables all unused variable linter warnings
Enables or disables unused variable linter warnings in function arguments
Enables or disables unused variable linter warnings in patterns
Enables linting variables defined in tactic blocks, may be expensive for complex proofs
Gets the status of linter.unusedVariables
Instances For
Gets the status of linter.unusedVariables.patternVars
Instances For
An IgnoreFunction
receives:
- a
Syntax.ident
for the unused variable - a
Syntax.Stack
with the location of this piece of syntax in the command - The
Options
set locally to this syntax
and should return true
to indicate that the lint should be suppressed,
or false
to proceed with linting as usual (other IgnoreFunction
s may still
say it is ignored). A variable is only linted if it is unused and no
IgnoreFunction
returns true
on this syntax.
Instances For
Interpret an IgnoreFunction
from the environment.
Instances For
Interpret an IgnoreFunction
from the environment.
The list of builtin IgnoreFunction
s.
Adds a new builtin IgnoreFunction
.
This function should only be used from within the Lean
package.
Instances For
An extension which keeps track of registered IgnoreFunction
s.
Collect into a heterogeneous collection of objects with external storage. This uses pointer identity and does not store the objects, so it is important not to store the last pointer to an object in the map, or it can be freed and reused, resulting in incorrect behavior.
Returns true
if the object was not already in the set.
Instances For
Collect into a heterogeneous collection of objects with external storage. This uses pointer identity and does not store the objects, so it is important not to store the last pointer to an object in the map, or it can be freed and reused, resulting in incorrect behavior.
Returns true
if the object was not already in the set.
Collects into fvarUses
all fvar
s occurring in the Expr
s in assignments
.
This implementation respects subterm sharing in both the PersistentHashMap
and the Expr
to ensure that pointer-equal subobjects are not visited multiple times, which is important
in practice because these expressions are very frequently highly shared.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Visit a PersistentHashMap.Node
, collecting all fvars in it into fvarUses
Visit a PersistentHashMap.Entry
, collecting all fvars in it into fvarUses
Visit an Expr
, collecting all fvars in it into fvarUses
Equations
- One or more equations did not get rendered due to their size.
Instances For
Given aliases
as a map from an alias to what it aliases, we get the original
term by recursion. This has no cycle detection, so if aliases
contains a loop
then this function will recurse infinitely.
Information regarding an FVarId
definition.
- userName : Lean.Name
The user-provided name of the
FVarId
- stx : Lean.Syntax
A (usually
.ident
) syntax for the defined variable - opts : Lean.Options
The options set locally to this part of the syntax (used by
IgnoreFn
) - aliases : Array Lean.FVarId
The list of all
FVarId
s that are considered as being defined at this position. This can have more than one element if multiple variables are declared by the same syntax, such as theh
inif h : c then ... else ...
. We only report an unused variable at this span if all variables inaliases
are unused.
Instances For
The main data structure used to collect information from the info tree regarding unused variables.
- constDecls : Std.HashSet String.Range
The set of all (ranges corresponding to) global definitions that are made in the syntax. For example in
mutual def foo := ... def bar := ... where baz := ... end
this would be the spans forfoo
,bar
, andbaz
. Global definitions are always treated as used. (It would be nice to be able to detect unused global definitions but this requires more information than the linter framework can provide.) The collection of all local declarations, organized by the span of the declaration. We collapse all declarations declared at the same position into a single record using
FVarDefinition.aliases
.- fvarUses : Std.HashSet Lean.FVarId
The set of
FVarId
s that are used directly. These may or may not be aliases. - fvarAliases : Std.HashMap Lean.FVarId Lean.FVarId
A mapping from alias to original FVarId. We don't guarantee that the value is not itself an alias, but we use
followAliases
when adding new elements to try to avoid long chains. - assignments : Array (Lean.PersistentHashMap Lean.MVarId Lean.Expr)
Collection of all
MetavarContext
s following the execution of a tactic. We trawl these if needed to find additionalfvarUses
.
Instances For
Collect information from the infoTrees
into References
.
See References
for more information about the return value.
Instances For
Since declarations attach the declaration info to the declId
,
we skip that to get to the .ident
if possible.
Instances For
Reports unused variable warnings on each command. Use linter.unusedVariables
to disable.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Returns true if this is a message produced by the unused variable linter. This is used to give these messages an additional "faded" style in the editor.
Equations
- msg.isUnusedVariableWarning = Lean.MessageData.hasTag (fun (x : Lean.Name) => x == Lean.Linter.linter.unusedVariables.name) msg