This came up when watching new Lean users in a class situation. A number of them were confused when they omitted a namespace on a constructor name, and Lean treated the variable as a pattern that matches anything. For example, this program is accepted but may not do what the user thinks: ``` inductive Tree (α : Type) where | leaf | branch (left : Tree α) (val : α) (right : Tree α) def depth : Tree α → Nat | leaf => 0 ``` Adding a `branch` case to `depth` results in a confusing message. With this linter, Lean marks `leaf` with: ``` Local variable 'leaf' resembles constructor 'Tree.leaf' - write '.leaf' (with a dot) or 'Tree.leaf' to use the constructor. note: this linter can be disabled with `set_option linter.constructorNameAsVariable false` ``` Additionally, the error message that occurs when invalid names are applied in patterns now suggests similar names. This means that: ``` def length (list : List α) : Nat := match list with | nil => 0 | cons x xs => length xs + 1 ``` now results in the following warning on `nil`: ``` warning: Local variable 'nil' resembles constructor 'List.nil' - write '.nil' (with a dot) or 'List.nil' to use the constructor. note: this linter can be disabled with `set_option linter.constructorNameAsVariable false` ``` and error on `cons`: ``` invalid pattern, constructor or constant marked with '[match_pattern]' expected Suggestion: 'List.cons' is similar ``` The list of suggested constructors is generated before the type of the pattern is known, so it's less accurate, but it truncates the list to ten elements to avoid being overwhelming. This mostly comes up with `mk`.
83 lines
3.7 KiB
Text
83 lines
3.7 KiB
Text
/-
|
||
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
|
||
-/
|
||
prelude
|
||
import Lean.Elab.Command
|
||
import Lean.Linter.Util
|
||
|
||
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 (Lean.HashMap String.Range (Syntax × Name × Name)) ← IO.mkRef {}
|
||
|
||
for tree in infoTrees do
|
||
tree.visitM' (preNode := 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
|