Skip to content

Commit 4e59cdf

Browse files
authored
chore: turn defLemma linter into Lean.Linter and rename it to defProp (#13803)
This PR renames the `defLemma` linter to `defProp` and clarifies its warning message. The linter is also moved out of the `EnvLinter` framework into its own file `Lean.Linter.DefProp`, since it now processes elaboration info trees per command. The helper `getDeclsByBody` is exposed in `Lean.Linter.Util` for reuse.
1 parent f6d1212 commit 4e59cdf

10 files changed

Lines changed: 111 additions & 53 deletions

File tree

src/Lean/Linter.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ prelude
99
public import Lean.Linter.Util
1010
public import Lean.Linter.Builtin
1111
public import Lean.Linter.ConstructorAsVariable
12+
public import Lean.Linter.DefProp
1213
public import Lean.Linter.Deprecated
1314
public import Lean.Linter.DocsOnAlt
1415
public import Lean.Linter.UnusedVariables

src/Lean/Linter/DefProp.lean

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/-
2+
Copyright (c) 2026 Lean FRO, LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Wojciech Różowski
5+
6+
Copyright (c) 2020 Floris van Doorn. All rights reserved.
7+
Released under Apache 2.0 license as described in the file LICENSE.
8+
Authors: Floris van Doorn, Robert Y. Lewis, Gabriel Ebner
9+
-/
10+
module
11+
12+
prelude
13+
public import Lean.Linter.Basic
14+
public import Lean.Linter.Util
15+
16+
public section
17+
18+
namespace Lean.Linter
19+
open Elab Command Meta Term
20+
21+
/--
22+
Enables the `defProp` linter, which warns when a `def` is used to introduce a
23+
declaration whose type is a `Prop`. Such a declaration should be written using
24+
`theorem` instead.
25+
-/
26+
register_builtin_option linter.defProp : Bool := {
27+
defValue := false
28+
descr := "enable the `defProp` linter, which warns when a `def` is used to introduce \
29+
a declaration whose type is a `Prop`; such a declaration should be written using \
30+
`theorem` instead."
31+
}
32+
33+
namespace DefProp
34+
35+
@[inherit_doc linter.defProp]
36+
def defPropLinter : Linter where run := withSetOptionIn fun _ => do
37+
unless getLinterValue linter.defProp (← getLinterOptions) do
38+
return
39+
if (← get).messages.hasErrors then
40+
return
41+
let env ← getEnv
42+
for t in ← getInfoTrees do
43+
for declName in getDeclsByBody t do
44+
let some info := env.find? declName | continue
45+
unless info.isDefinition do continue
46+
let isPropType ← liftTermElabM <| isProp info.type
47+
if isPropType then
48+
logLintIf linter.defProp (← getRef) m!"\
49+
Definition `{.ofConstName declName}` is a proposition; use `theorem` instead of `def`"
50+
51+
builtin_initialize addLinter defPropLinter
52+
53+
end DefProp
54+
end Lean.Linter

src/Lean/Linter/EnvLinter/Builtin.lean

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,6 @@ public meta section
2525

2626
namespace Lean.Linter.EnvLinter
2727

28-
/-- A linter for checking whether the correct declaration constructor (`def` or `theorem`)
29-
has been used. A `def` whose type is a `Prop` should be a `theorem`, and vice versa. -/
30-
@[builtin_env_linter] def defLemma : EnvLinter where
31-
noErrorsFound := "All declarations correctly marked as def/lemma."
32-
errorsFound := "INCORRECT DEF/LEMMA:"
33-
test declName := do
34-
if ← isAutoDecl declName then return none
35-
let info ← getConstInfo declName
36-
if info.isDefinition then
37-
if ← isProp info.type then return some "is a def, should be a lemma/theorem"
38-
return none
39-
4028
/--
4129
`univParamsGrouped e nm₀` computes for each `Level` occurring in `e` the set of level parameters
4230
that appear in it, returning the collection of such parameter sets.

src/Lean/Linter/Extra/UnusedDecidableInType.lean

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public import Lean.Meta.ForEachExpr
1515
public import Lean.Meta.Sorry
1616
public import Lean.PrivateName
1717
public import Lean.Server.InfoUtils
18+
public import Lean.Linter.Util
1819

1920
public section
2021

@@ -253,18 +254,6 @@ private def onUnusedInstancesWhere (decl : ConstantVal) (p : Expr → Bool)
253254
}
254255
logOnUnused unusedParams
255256

256-
/-- Get the `parentDecl`s of every elaborated body in the infotree. -/
257-
private def getDeclsByBody (t : InfoTree) : List Name :=
258-
t.collectNodesBottomUp fun ctx i _ decls =>
259-
match i with
260-
| .ofCustomInfo i =>
261-
if i.value.typeName == ``Lean.Elab.Term.BodyInfo then
262-
if let some decl := ctx.parentDecl? then
263-
decl :: decls
264-
else decls
265-
else decls
266-
| _ => decls
267-
268257
/--
269258
Get the declarations elaborated in the infotree `t` which are theorems according to the
270259
environment. This includes e.g. `instance`s of `Prop` classes in addition to declarations declared

src/Lean/Linter/Util.lean

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module
88
prelude
99
public import Lean.Server.InfoUtils
1010
public import Lean.Linter.Init
11+
public import Lean.Elab.Term
1112

1213
public section
1314

@@ -44,3 +45,15 @@ where
4445
return some []
4546
else
4647
return none)
48+
49+
/-- Get the `parentDecl`s of every elaborated body in the infotree. -/
50+
def getDeclsByBody (t : InfoTree) : List Name :=
51+
t.collectNodesBottomUp fun ctx i _ decls =>
52+
match i with
53+
| .ofCustomInfo i =>
54+
if i.value.typeName == ``Lean.Elab.Term.BodyInfo then
55+
if let some decl := ctx.parentDecl? then
56+
decl :: decls
57+
else decls
58+
else decls
59+
| _ => decls

tests/elab/env_linter.lean

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def testGetChecksDefault : CoreM (Array Name) := do
131131
return checks.map (·.name)
132132

133133
-- dummyBadName and checkUnivs are default, dummyExtraLinter is not
134-
/-- info: #[`checkUnivs, `defLemma, `dummyBadName] -/
134+
/-- info: #[`checkUnivs, `dummyBadName] -/
135135
#guard_msgs in
136136
#eval testGetChecksDefault
137137

@@ -140,7 +140,7 @@ def testGetChecksExtra : CoreM (Array Name) := do
140140
let checks ← getChecks (scope := .extra) (runOnly := none)
141141
return checks.map (·.name)
142142

143-
/-- info: #[`checkUnivs, `defLemma, `dummyBadName, `dummyExtraLinter] -/
143+
/-- info: #[`checkUnivs, `dummyBadName, `dummyExtraLinter] -/
144144
#guard_msgs in
145145
#eval testGetChecksExtra
146146

@@ -149,7 +149,7 @@ def testGetChecksAll : CoreM (Array Name) := do
149149
let checks ← getChecks (scope := .all) (runOnly := none)
150150
return checks.map (·.name)
151151

152-
/-- info: #[`checkUnivs, `defLemma, `dummyBadName, `dummyExtraLinter] -/
152+
/-- info: #[`checkUnivs, `dummyBadName, `dummyExtraLinter] -/
153153
#guard_msgs in
154154
#eval testGetChecksAll
155155

@@ -181,7 +181,7 @@ def testLintCore : CoreM (Array (Name × Nat)) := do
181181
let results ← lintCore #[`badDef, `goodDef, `badButNolinted] linters
182182
return results.map fun (linter, msgs) => (linter.name, msgs.size)
183183

184-
/-- info: #[(`checkUnivs, 0), (`defLemma, 0), (`dummyBadName, 1)] -/
184+
/-- info: #[(`checkUnivs, 0), (`dummyBadName, 1)] -/
185185
#guard_msgs in
186186
#eval testLintCore
187187

@@ -210,7 +210,7 @@ def testFormatResults : CoreM Format := do
210210
return (← msg.format)
211211

212212
/--
213-
info: -- Found 1 error in 2 declarations (plus 0 automatically generated ones) in test with 3 linters
213+
info: -- Found 1 error in 2 declarations (plus 0 automatically generated ones) in test with 2 linters
214214
215215
/- The `dummyBadName` linter reports:
216216
found bad names -/
@@ -229,7 +229,7 @@ def testFormatResultsClean : CoreM Format := do
229229
return (← msg.format)
230230

231231
/--
232-
info: -- Found 0 errors in 1 declarations (plus 0 automatically generated ones) in test with 3 linters
232+
info: -- Found 0 errors in 1 declarations (plus 0 automatically generated ones) in test with 2 linters
233233
-/
234234
#guard_msgs in
235235
#eval testFormatResultsClean

tests/lake/tests/builtin-lint/ExtraViolations.lean

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import Linters
22

3+
-- `linter.defProp` is off by default for bootstrapping reasons; enable it
4+
-- here so `shouldBeTheoremUnderExtra` still fires when default linters run.
5+
set_option linter.defProp true
6+
37
-- This name ends with 'Extra' — the dummyExtra linter should flag it.
48
def badNameExtra : Nat := 1
59

@@ -13,7 +17,7 @@ example : True := by
1317
-- the builtin extra `dupNamespace` text linter should flag it.
1418
def Dup.Dup.violation : Nat := 2
1519

16-
-- This uses `def` for a Prop — the default `defLemma` linter should flag this
20+
-- This uses `def` for a Prop — the default `defProp` linter should flag this
1721
-- whenever default linters run, including under `--extra`.
1822
def shouldBeTheoremUnderExtra : 1 = 1 := rfl
1923

tests/lake/tests/builtin-lint/Main.lean

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import Main.Sub
22

3-
-- This uses `def` for a Prop — the `defLemma` linter should flag this.
3+
-- `linter.defProp` is off by default for bootstrapping reasons; enable it
4+
-- here so the test scenarios that exercise default lint mode still trigger it.
5+
set_option linter.defProp true
6+
7+
-- This uses `def` for a Prop — the `defProp` linter should flag this.
48
def shouldBeTheorem : 1 = 1 := rfl
59

6-
-- This is annotated to be skipped by `defLemma` — no import needed.
7-
@[builtin_nolint defLemma]
10+
-- `set_option` disables `defProp` locally so this violation is not flagged.
11+
set_option linter.defProp false in
812
def skippedViolation : 2 = 2 := rfl
913

1014
-- A `@[reducible, instance] def` of Prop type is still elaborated as a `def`,
11-
-- so `defLemma` should flag it.
15+
-- so `defProp` should flag it.
1216
@[reducible, instance]
1317
def reducibleInstShouldBeTheorem : Nonempty Bool := ⟨true
1418

tests/lake/tests/builtin-lint/Main/Sub.lean

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
-- Env-linter violation: `def` on a Prop — should be caught by `defLemma`
2-
-- regardless of build-time options, since env linters run post-build.
1+
-- `linter.defProp` is off by default for bootstrapping reasons; enable it
2+
-- here so this transitive-import test still captures the violation in the
3+
-- module's lint log.
4+
set_option linter.defProp true
5+
6+
-- `def` on a Prop — caught by the `defProp` linter at build time.
37
def shouldBeTheoremInSub : 1 = 1 := rfl
48

59
-- Default text-linter violation: `linter.unusedVariables` has `defValue := true`,

tests/lake/tests/builtin-lint/test.sh

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ lake_out lint --lint-only unusedVariables TextLints || true
3030
match_pat 'Variable name `unusedLet` is not explicitly referenced' produced.out
3131
no_match_pat 'missing doc string' produced.out
3232

33-
# --builtin-lint should detect the defLemma violation in Main (the default target)
33+
# --builtin-lint should detect the defProp violation in Main (the default target)
3434
lake_out lint --builtin-lint || true
3535
match_pat 'shouldBeTheorem' produced.out
36-
match_pat 'is a def, should be a lemma/theorem' produced.out
36+
match_pat 'is a proposition; use `theorem` instead of `def`' produced.out
3737
# `@[reducible, instance]` on a `def` of Prop type keeps it a `def`, so flag it.
3838
match_pat 'reducibleInstShouldBeTheorem' produced.out
3939
# Plain `instance` of Prop type is elaborated as a theorem; it should not be flagged.
@@ -45,8 +45,8 @@ match_pat 'only occur together' produced.out
4545
# builtin_nolint checkUnivs should suppress the warning
4646
no_match_pat 'badUnivSkipped' produced.out
4747

48-
# --lint-only defLemma should run only the defLemma linter
49-
lake_out lint --lint-only defLemma || true
48+
# --lint-only defProp should run only the defProp linter
49+
lake_out lint --lint-only defProp || true
5050
match_pat 'shouldBeTheorem' produced.out
5151
no_match_pat 'badUnivDecl' produced.out
5252

@@ -57,8 +57,9 @@ no_match_pat 'badUnivDecl' produced.out
5757
# package, so passing any module of a package flips the flag for every module
5858
# in that package.
5959

60-
# Env linters run post-build against `importModules`-loaded decls, so
61-
# `defLemma` catches `shouldBeTheoremInSub` regardless of override scope.
60+
# `defProp` runs during the build of each module, so its warning for
61+
# `shouldBeTheoremInSub` is captured in `Main.Sub`'s lint log and re-emitted
62+
# via `collectTextLints` when `Main` is linted.
6263
lake_out lint --builtin-lint Main || true
6364
match_pat 'shouldBeTheoremInSub' produced.out
6465

@@ -81,7 +82,7 @@ test_run lint --builtin-only Clean
8182

8283
# Without --extra, the extra linters (both the env linter and the dummy extra
8384
# text linter in Linters.lean) must not run. Default linters still do, so the
84-
# `defLemma` violation in this file fires.
85+
# default `defProp` linter's violation in this file fires.
8586
lake_out lint --builtin-only ExtraViolations || true
8687
no_match_pat 'badNameExtra' produced.out
8788
no_match_pat 'extra text linter saw a declaration' produced.out
@@ -91,7 +92,7 @@ no_match_pat 'tac1 <;> tac2' produced.out
9192
no_match_pat 'Dup.Dup.violation' produced.out
9293
# Builtin extra text linter `unreachableTactic` is non-default, so silent.
9394
no_match_pat 'this tactic is never executed' produced.out
94-
# Default env linter `defLemma` runs and flags the def-of-Prop in this file.
95+
# Default `defProp` linter runs and flags the def-of-Prop in this file.
9596
match_pat 'shouldBeTheoremUnderExtra' produced.out
9697

9798
# --extra should run default linters together with the non-default (extra)
@@ -109,7 +110,7 @@ match_pat 'Dup.Dup.violation' produced.out
109110
match_pat "namespace .*Dup.* is duplicated" produced.out
110111
# Builtin `unreachableTactic` extra text linter fires under --extra.
111112
match_pat 'this tactic is never executed' produced.out
112-
# --extra also runs default linters, so `defLemma` flags this file's violation.
113+
# --extra also runs default linters, so `defProp` flags this file's violation.
113114
match_pat 'shouldBeTheoremUnderExtra' produced.out
114115

115116
# --extra on TextLints: default `linter.unusedVariables` fires (default
@@ -141,15 +142,15 @@ no_match_pat 'badNameExtra' produced.out
141142
no_match_pat 'shouldBeTheorem' produced.out
142143

143144
# Multiple --lint-only flags accumulate: both named linters should run
144-
lake_out lint --lint-only defLemma --lint-only checkUnivs || true
145+
lake_out lint --lint-only defProp --lint-only checkUnivs || true
145146
match_pat 'shouldBeTheorem' produced.out
146147
match_pat 'badUnivDecl' produced.out
147148
no_match_pat 'badNameExtra' produced.out
148149

149150
# Last-wins: --extra overrides a prior --lint-all and clears --lint-only.
150-
# Since --extra runs both default and extra linters, the default `defLemma`
151+
# Since --extra runs both default and extra linters, the default `defProp`
151152
# violation in ExtraViolations.lean fires too.
152-
lake_out lint --lint-all --lint-only defLemma --extra || true
153+
lake_out lint --lint-all --lint-only defProp --extra || true
153154
match_pat 'badNameExtra' produced.out
154155
match_pat 'shouldBeTheoremUnderExtra' produced.out
155156

@@ -160,13 +161,13 @@ match_pat 'badNameExtra' produced.out
160161
match_pat 'shouldBeTheorem' produced.out
161162

162163
# Last-wins: --extra clears a previously accumulated --lint-only. Default
163-
# linters still run under --extra, so `defLemma` fires on its file's violation.
164-
lake_out lint --lint-only defLemma --extra || true
164+
# linters still run under --extra, so `defProp` fires on its file's violation.
165+
lake_out lint --lint-only defProp --extra || true
165166
match_pat 'badNameExtra' produced.out
166167
match_pat 'shouldBeTheoremUnderExtra' produced.out
167168

168169
# --lint-only after --extra: the named linter runs (selection ignores scope)
169-
lake_out lint --extra --lint-only defLemma || true
170+
lake_out lint --extra --lint-only defProp || true
170171
match_pat 'shouldBeTheorem' produced.out
171172
no_match_pat 'badNameExtra' produced.out
172173

@@ -193,7 +194,7 @@ lake_out lint --extra ExtraViolations || true
193194
match_pat 'badNameExtra' produced.out
194195

195196
# --lint-only implicitly enables builtin lint
196-
lake_out lint --lint-only defLemma || true
197+
lake_out lint --lint-only defProp || true
197198
match_pat 'shouldBeTheorem' produced.out
198199

199200
# builtinLint = false: check-lint fails (no lint driver and builtin linting disabled)

0 commit comments

Comments
 (0)