This replaces the no-op `unusedVariablesIgnoreFnsExt` environment extension with an actual environment extension which can be extended using either `@[unused_variables_ignore_fn]` or `@[builtin_unused_variables_ignore_fn]` (although for the present all the builtin `unused_variables_ignore_fn`s are being added using direct calls to `builtin_initialize addBuiltinUnusedVariablesIgnoreFn`, because this also works and a stage0 update is required before the attribute can be used). We would like to use this attribute to disable unused variables in syntaxes defined in std and mathlib, like [`proof_wanted`](https://leanprover.zulipchat.com/#narrow/stream/113488-general/topic/Unused.20variables.20and.20proof_wanted/near/408554690).
291 lines
12 KiB
Text
291 lines
12 KiB
Text
import Lean.Elab.Command
|
||
import Lean.Util.ForEachExpr
|
||
import Lean.Linter.Util
|
||
import Lean.Server.References
|
||
|
||
namespace Lean.Linter
|
||
open Lean.Elab.Command Lean.Server Std
|
||
|
||
register_builtin_option linter.unusedVariables : Bool := {
|
||
defValue := true,
|
||
descr := "enable the 'unused variables' linter"
|
||
}
|
||
register_builtin_option linter.unusedVariables.funArgs : Bool := {
|
||
defValue := true,
|
||
descr := "enable the 'unused variables' linter to mark unused function arguments"
|
||
}
|
||
register_builtin_option linter.unusedVariables.patternVars : Bool := {
|
||
defValue := true,
|
||
descr := "enable the 'unused variables' linter to mark unused pattern variables"
|
||
}
|
||
|
||
def getLinterUnusedVariables (o : Options) : Bool := getLinterValue linter.unusedVariables o
|
||
def getLinterUnusedVariablesFunArgs (o : Options) : Bool := o.get linter.unusedVariables.funArgs.name (getLinterUnusedVariables o)
|
||
def getLinterUnusedVariablesPatternVars (o : Options) : Bool := o.get linter.unusedVariables.patternVars.name (getLinterUnusedVariables o)
|
||
|
||
abbrev IgnoreFunction := Syntax → Syntax.Stack → Options → Bool
|
||
|
||
unsafe def mkIgnoreFnImpl (constName : Name) : ImportM IgnoreFunction := do
|
||
let { env, opts, .. } ← read
|
||
match env.find? constName with
|
||
| none => throw ↑s!"unknown constant '{constName}'"
|
||
| some info =>
|
||
unless info.type.isConstOf ``IgnoreFunction do
|
||
throw ↑s!"unexpected unused_variables_ignore_fn at '{constName}', must be of type `Lean.Linter.IgnoreFunction`"
|
||
IO.ofExcept <| env.evalConst IgnoreFunction opts constName
|
||
|
||
@[implemented_by mkIgnoreFnImpl]
|
||
opaque mkIgnoreFn (constName : Name) : ImportM IgnoreFunction
|
||
|
||
builtin_initialize builtinUnusedVariablesIgnoreFnsRef : IO.Ref <| Array IgnoreFunction ← IO.mkRef #[]
|
||
|
||
def addBuiltinUnusedVariablesIgnoreFn (h : IgnoreFunction) : IO Unit :=
|
||
builtinUnusedVariablesIgnoreFnsRef.modify (·.push h)
|
||
|
||
builtin_initialize unusedVariablesIgnoreFnsExt :
|
||
PersistentEnvExtension Name (Name × IgnoreFunction) (List Name × Array IgnoreFunction) ←
|
||
registerPersistentEnvExtension {
|
||
mkInitial := return ([], ← builtinUnusedVariablesIgnoreFnsRef.get)
|
||
addImportedFn := fun as => do
|
||
([], ·) <$> as.foldlM (init := ← builtinUnusedVariablesIgnoreFnsRef.get) fun s as =>
|
||
as.foldlM (init := s) fun s n => s.push <$> mkIgnoreFn n
|
||
addEntryFn := fun (entries, s) (n, h) => (n::entries, s.push h)
|
||
exportEntriesFn := fun s => s.1.reverse.toArray
|
||
statsFn := fun s => format "number of local entries: " ++ format s.1.length
|
||
}
|
||
|
||
builtin_initialize
|
||
let mkAttr (builtin : Bool) (name : Name) := registerBuiltinAttribute {
|
||
name
|
||
descr := (if builtin then "(builtin) " else "") ++
|
||
"Marks a function of type `Lean.Linter.IgnoreFunction` for suppressing unused variable warnings"
|
||
applicationTime := .afterCompilation
|
||
add := fun decl stx kind => do
|
||
Attribute.Builtin.ensureNoArgs stx
|
||
unless kind == AttributeKind.global do throwError "invalid attribute '{name}', must be global"
|
||
unless (← getConstInfo decl).type.isConstOf ``IgnoreFunction do
|
||
throwError "invalid attribute '{name}', must be of type `Lean.Linter.IgnoreFunction`"
|
||
let env ← getEnv
|
||
if builtin then
|
||
let h := mkConst decl
|
||
declareBuiltin decl <| mkApp (mkConst ``addBuiltinUnusedVariablesIgnoreFn) h
|
||
else
|
||
setEnv <| unusedVariablesIgnoreFnsExt.addEntry env (decl, ← mkIgnoreFn decl)
|
||
}
|
||
mkAttr true `builtin_unused_variables_ignore_fn
|
||
mkAttr false `unused_variables_ignore_fn
|
||
|
||
-- matches builtinUnused variable pattern
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn fun stx _ _ =>
|
||
stx.getId.toString.startsWith "_"
|
||
|
||
-- is variable
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack _ =>
|
||
stack.matches [`null, none, `null, ``Lean.Parser.Command.variable])
|
||
|
||
-- is in structure
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack _ =>
|
||
stack.matches [`null, none, `null, ``Lean.Parser.Command.structure])
|
||
|
||
-- is in inductive
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack _ =>
|
||
stack.matches [`null, none, `null, none, ``Lean.Parser.Command.inductive] &&
|
||
(stack.get? 3 |>.any fun (stx, pos) =>
|
||
pos == 0 &&
|
||
[``Lean.Parser.Command.optDeclSig, ``Lean.Parser.Command.declSig].any (stx.isOfKind ·)))
|
||
|
||
-- in in constructor or structure binder
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack _ =>
|
||
stack.matches [`null, none, `null, ``Lean.Parser.Command.optDeclSig, none] &&
|
||
(stack.get? 4 |>.any fun (stx, _) =>
|
||
[``Lean.Parser.Command.ctor, ``Lean.Parser.Command.structSimpleBinder].any (stx.isOfKind ·)))
|
||
|
||
-- is in opaque or axiom
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack _ =>
|
||
stack.matches [`null, none, `null, ``Lean.Parser.Command.declSig, none] &&
|
||
(stack.get? 4 |>.any fun (stx, _) =>
|
||
[``Lean.Parser.Command.opaque, ``Lean.Parser.Command.axiom].any (stx.isOfKind ·)))
|
||
|
||
-- is in definition with foreign definition
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack _ =>
|
||
stack.matches [`null, none, `null, none, none, ``Lean.Parser.Command.declaration] &&
|
||
(stack.get? 3 |>.any fun (stx, _) =>
|
||
stx.isOfKind ``Lean.Parser.Command.optDeclSig ||
|
||
stx.isOfKind ``Lean.Parser.Command.declSig) &&
|
||
(stack.get? 5 |>.any fun (stx, _) => match stx[0] with
|
||
| `(Lean.Parser.Command.declModifiersT| $[$_:docComment]? @[$[$attrs:attr],*] $[$vis]? $[noncomputable]?) =>
|
||
attrs.any (fun attr => attr.raw.isOfKind ``Parser.Attr.extern || attr matches `(attr| implemented_by $_))
|
||
| _ => false))
|
||
|
||
-- is in dependent arrow
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack _ =>
|
||
stack.matches [`null, ``Lean.Parser.Term.explicitBinder, ``Lean.Parser.Term.depArrow])
|
||
|
||
-- is in let declaration
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack opts =>
|
||
!getLinterUnusedVariablesFunArgs opts &&
|
||
stack.matches [`null, none, `null, ``Lean.Parser.Term.letIdDecl, none] &&
|
||
(stack.get? 3 |>.any fun (_, pos) => pos == 1) &&
|
||
(stack.get? 5 |>.any fun (stx, _) => !stx.isOfKind ``Lean.Parser.Command.whereStructField))
|
||
|
||
-- is in declaration signature
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack opts =>
|
||
!getLinterUnusedVariablesFunArgs opts &&
|
||
stack.matches [`null, none, `null, none] &&
|
||
(stack.get? 3 |>.any fun (stx, pos) =>
|
||
pos == 0 &&
|
||
[``Lean.Parser.Command.optDeclSig, ``Lean.Parser.Command.declSig].any (stx.isOfKind ·)))
|
||
|
||
-- is in function definition
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack opts =>
|
||
!getLinterUnusedVariablesFunArgs opts &&
|
||
(stack.matches [`null, ``Lean.Parser.Term.basicFun] ||
|
||
stack.matches [``Lean.Parser.Term.typeAscription, `null, ``Lean.Parser.Term.basicFun]))
|
||
|
||
-- is pattern variable
|
||
builtin_initialize addBuiltinUnusedVariablesIgnoreFn (fun _ stack opts =>
|
||
!getLinterUnusedVariablesPatternVars opts &&
|
||
stack.any fun (stx, pos) =>
|
||
(stx.isOfKind ``Lean.Parser.Term.matchAlt && pos == 1) ||
|
||
(stx.isOfKind ``Lean.Parser.Tactic.inductionAltLHS && pos == 2))
|
||
|
||
unsafe def getUnusedVariablesIgnoreFnsImpl : CommandElabM (Array IgnoreFunction) := do
|
||
return (unusedVariablesIgnoreFnsExt.getState (← getEnv)).2
|
||
|
||
@[implemented_by getUnusedVariablesIgnoreFnsImpl]
|
||
opaque getUnusedVariablesIgnoreFns : CommandElabM (Array IgnoreFunction)
|
||
|
||
|
||
def unusedVariables : Linter where
|
||
run cmdStx := do
|
||
unless getLinterUnusedVariables (← getOptions) do
|
||
return
|
||
|
||
-- NOTE: `messages` is local to the current command
|
||
if (← get).messages.hasErrors then
|
||
return
|
||
|
||
let some cmdStxRange := cmdStx.getRange?
|
||
| pure ()
|
||
|
||
let infoTrees := (← get).infoState.trees.toArray
|
||
let fileMap := (← read).fileMap
|
||
|
||
if (← infoTrees.anyM (·.hasSorry)) then
|
||
return
|
||
|
||
-- collect references
|
||
let refs := findModuleRefs fileMap infoTrees (allowSimultaneousBinderUse := true)
|
||
|
||
let mut vars : HashMap FVarId RefInfo := .empty
|
||
let mut constDecls : HashSet String.Range := .empty
|
||
|
||
for (ident, info) in refs.toList do
|
||
match ident with
|
||
| .fvar id =>
|
||
vars := vars.insert id info
|
||
| .const _ =>
|
||
if let some definition := info.definition then
|
||
if let some range := definition.stx.getRange? then
|
||
constDecls := constDecls.insert range
|
||
|
||
-- collect uses from tactic infos
|
||
let tacticMVarAssignments : HashMap MVarId Expr :=
|
||
infoTrees.foldr (init := .empty) fun tree assignments =>
|
||
tree.foldInfo (init := assignments) (fun _ i assignments => match i with
|
||
| .ofTacticInfo ti =>
|
||
ti.mctxAfter.eAssignment.foldl (init := assignments) fun assignments mvar expr =>
|
||
if assignments.contains mvar then
|
||
assignments
|
||
else
|
||
assignments.insert mvar expr
|
||
| _ =>
|
||
assignments)
|
||
|
||
-- collect fvars from mvar assignments
|
||
let tacticFVarUses : HashSet FVarId ←
|
||
Elab.Command.liftIO <| -- no need to carry around other state here
|
||
StateT.run' (s := HashSet.empty) do
|
||
-- use one big cache for all `ForEachExpr.visit` invocations
|
||
MonadCacheT.run do
|
||
tacticMVarAssignments.forM fun _ e =>
|
||
ForEachExpr.visit (e := e) fun e => do
|
||
if e.isFVar then modify (·.insert e.fvarId!)
|
||
return e.hasFVar
|
||
get
|
||
|
||
-- collect ignore functions
|
||
let ignoreFns ← getUnusedVariablesIgnoreFns
|
||
let ignoreFns declStx stack opts :=
|
||
isTopLevelDecl constDecls declStx stack opts ||
|
||
ignoreFns.any (· declStx stack opts)
|
||
|
||
-- determine unused variables
|
||
let mut unused := #[]
|
||
for (id, ⟨decl?, uses⟩) in vars.toList do
|
||
-- process declaration
|
||
let some decl := decl?
|
||
| continue
|
||
let declStx := skipDeclIdIfPresent decl.stx
|
||
let some range := declStx.getRange?
|
||
| continue
|
||
let some localDecl := decl.info.lctx.find? id
|
||
| continue
|
||
if !cmdStxRange.contains range.start || localDecl.userName.hasMacroScopes then
|
||
continue
|
||
|
||
-- check if variable is used
|
||
if !uses.isEmpty || tacticFVarUses.contains id || decl.aliases.any (match · with | .fvar id => tacticFVarUses.contains id | _ => false) then
|
||
continue
|
||
|
||
-- check linter options
|
||
let opts := decl.ci.options
|
||
if !getLinterUnusedVariables opts then
|
||
continue
|
||
|
||
-- evaluate ignore functions on original syntax
|
||
if let some ((id', _) :: stack) := cmdStx.findStack? (·.getRange?.any (·.includes range)) then
|
||
if id'.isIdent && ignoreFns declStx stack opts then
|
||
continue
|
||
else
|
||
continue
|
||
|
||
-- evaluate ignore functions on macro expansion outputs
|
||
if ← infoTrees.anyM fun tree => do
|
||
if let some macroExpansions ← collectMacroExpansions? range tree then
|
||
return macroExpansions.any fun expansion =>
|
||
-- in a macro expansion, there may be multiple leafs whose (synthetic) range includes `range`, so accept strict matches only
|
||
if let some (_ :: stack) := expansion.output.findStack? (·.getRange?.any (·.includes range)) (fun stx => stx.isIdent && stx.getRange?.any (· == range)) then
|
||
ignoreFns declStx stack opts
|
||
else
|
||
false
|
||
else
|
||
return false
|
||
then
|
||
continue
|
||
|
||
-- publish warning if variable is unused and not ignored
|
||
unused := unused.push (declStx, localDecl)
|
||
|
||
for (declStx, localDecl) in unused.qsort (·.1.getPos?.get! < ·.1.getPos?.get!) do
|
||
logLint linter.unusedVariables declStx m!"unused variable `{localDecl.userName}`"
|
||
|
||
return ()
|
||
where
|
||
skipDeclIdIfPresent (stx : Syntax) : Syntax :=
|
||
if stx.isOfKind ``Lean.Parser.Command.declId then
|
||
stx[0]
|
||
else
|
||
stx
|
||
isTopLevelDecl (constDecls : HashSet String.Range) : IgnoreFunction := fun stx stack _ => Id.run <| do
|
||
let some declRange := stx.getRange?
|
||
| false
|
||
constDecls.contains declRange &&
|
||
!stack.matches [``Lean.Parser.Term.letIdDecl]
|
||
|
||
builtin_initialize addLinter unusedVariables
|
||
|
||
end Linter
|
||
|
||
def MessageData.isUnusedVariableWarning (msg : MessageData) : Bool :=
|
||
msg.hasTag (· == Linter.linter.unusedVariables.name)
|