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)