/- Copyright (c) 2024 Lean FRO, LLC. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Authors: David Thrane Christiansen -/ module prelude public import Lean.Elab.Command public import Lean.Linter.Util public section set_option linter.missingDocs true namespace Lean.Linter open Lean Elab Command open Lean.Linter (logLint) /-- A linter that warns when bound variable names are the same as constructor names for their types, modulo namespaces. -/ register_builtin_option linter.constructorNameAsVariable : Bool := { defValue := true, descr := "enable the linter that warns when bound variable names are nullary constructor names" } /-- Reports when bound variables' names overlap with constructor names for their type. This is to warn especially new users that they have built a pattern that matches anything, rather than one that matches a particular constructor. Use `linter.constructorNameAsVariable` to disable. -/ def constructorNameAsVariable : Linter where run cmdStx := do let some cmdStxRange := cmdStx.getRange? | return let infoTrees := (← get).infoState.trees.toArray let warnings : IO.Ref (Std.HashMap Lean.Syntax.Range (Syntax × Name × Name)) ← IO.mkRef {} for tree in infoTrees do tree.visitM' (postNode := fun ci info _ => do match info with | .ofTermInfo ti => match ti.expr with | .fvar id .. => let some range := info.range? | return if (← warnings.get).contains range then return let .original .. := info.stx.getHeadInfo | return if ti.isBinder then -- This is a local variable declaration. let some ldecl := ti.lctx.find? id | return -- Skip declarations which are outside the command syntax range, like `variable`s -- (it would be confusing to lint these), or those which are macro-generated if !cmdStxRange.contains range.start || ldecl.userName.hasMacroScopes then return let opts := ci.options -- we have to check for the option again here because it can be set locally if !linter.constructorNameAsVariable.get opts then return if let n@(.str .anonymous s) := info.stx.getId then -- Check whether the type is an inductive type, and get its constructors let ty ← if let some t := ti.expectedType? then pure t else ti.runMetaM ci (Meta.inferType ti.expr) let ty ← ti.runMetaM ci (instantiateMVars ty >>= Meta.whnf) if let .const tn _ := ty.getAppFn' then if let some (.inductInfo i) := (← getEnv).find? tn then for c in i.ctors do -- Only warn when the constructor has 0 fields. Pattern variables can't be -- confused with constructors that want arguments. if let some (.ctorInfo ctorInfo) := (← getEnv).find? c then if ctorInfo.numFields > 0 then continue if let .str _ cn := c then if cn == s then warnings.modify (·.insert range (info.stx, n, c)) else pure () | _ => pure () | _ => pure ()) -- Sort the outputs by position for (_range, declStx, userName, ctorName) in (← warnings.get).toArray.qsort (·.1.start < ·.1.start) do logLint linter.constructorNameAsVariable declStx <| m!"Local variable '{userName}' resembles constructor '{ctorName}' - " ++ m!"write '.{userName}' (with a dot) or '{ctorName}' to use the constructor." builtin_initialize addLinter constructorNameAsVariable