Skip to content

Commit f7f73d2

Browse files
kim-emb-mehta
authored andcommitted
feat(Tactic/Linter): add auxLemma linter for auto-generated declaration references (leanprover-community#37364)
This PR adds a syntax linter that flags explicit references to auto-generated auxiliary declarations such as `_proof_N`, `match_N`, `_sizeOf_N` or `_aux_N`. These names are internal to the Lean elaborator and are not stable across refactors (e.g. reordering fields in a structure can renumber `_proof_` indices). The linter matches precisely: it requires the suffix after the prefix to be all digits, so `_proof_helper` would not be flagged. It only inspects identifier syntax nodes, so references in comments and docstrings are ignored. Current hits in Mathlib (suppressed with `set_option linter.style.auxLemma false`): - `Mathlib/CategoryTheory/Functor/Category.lean` — `hcomp._proof_2` - `Mathlib/Analysis/Calculus/FormalMultilinearSeries.lean` — `constFormalMultilinearSeries.match_1.eq_2` - `Mathlib/Data/List/Sigma.lean` — `_sizeOf_1` Companion PR to track these suppressions in the tech debt report: leanprover-community/mathlib-ci#17 🤖 Prepared with Claude Code
1 parent c08d968 commit f7f73d2

11 files changed

Lines changed: 190 additions & 1 deletion

File tree

Mathlib.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7227,6 +7227,7 @@ public import Mathlib.Tactic.LinearCombination
72277227
public import Mathlib.Tactic.LinearCombination.Lemmas
72287228
public import Mathlib.Tactic.LinearCombinationPrime
72297229
public import Mathlib.Tactic.Linter
7230+
public import Mathlib.Tactic.Linter.AuxLemma
72307231
public import Mathlib.Tactic.Linter.CommandRanges
72317232
public import Mathlib.Tactic.Linter.CommandStart
72327233
public import Mathlib.Tactic.Linter.DeprecatedModule

Mathlib/Analysis/Calculus/FormalMultilinearSeries.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ theorem constFormalMultilinearSeries_apply_of_nonzero [NontriviallyNormedField
388388
{n : ℕ} (hn : n ≠ 0) : constFormalMultilinearSeries 𝕜 E c n = 0 :=
389389
Nat.casesOn n (fun hn => (hn rfl).elim) (fun _ _ => rfl) hn
390390

391+
set_option linter.auxLemma false in
391392
@[simp]
392393
lemma constFormalMultilinearSeries_zero [NontriviallyNormedField 𝕜] [NormedAddCommGroup E]
393394
[NormedAddCommGroup F] [NormedSpace 𝕜 E] [NormedSpace 𝕜 F] :

Mathlib/CategoryTheory/Functor/Category.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def hcomp {H I : D ⥤ E} (α : F ⟶ G) (β : H ⟶ I) : F ⋙ H ⟶ G ⋙ I wh
120120

121121
-- Horizontal composition has two possible definitions that are dual to each other,
122122
-- and we need to prove to `to_dual` that these are equivalent.
123+
set_option linter.auxLemma false in
123124
attribute [to_dual none] hcomp._proof_2 hcomp._proof_3
124125
to_dual_insert_cast hcomp := by ext x; exact β.naturality' (α.app x)
125126

Mathlib/Data/List/Sigma.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,7 @@ theorem kerase_comm (a₁ a₂) (l : List (Sigma β)) :
547547
else by simp [ha₂, mt mem_keys_of_mem_keys_kerase ha₂]
548548
else by simp [ha₁, mt mem_keys_of_mem_keys_kerase ha₁]
549549

550+
set_option linter.auxLemma false in
550551
theorem sizeOf_kerase [SizeOf (Sigma β)] (x : α)
551552
(xs : List (Sigma β)) : SizeOf.sizeOf (List.kerase x xs) ≤ SizeOf.sizeOf xs := by
552553
simp only [SizeOf.sizeOf, _sizeOf_1]
@@ -646,6 +647,7 @@ theorem dlookup_dedupKeys (a : α) (l : List (Sigma β)) : dlookup a (dedupKeys
646647
· rw [dedupKeys_cons, dlookup_kinsert_ne h, l_ih, dlookup_cons_ne]
647648
exact h
648649

650+
set_option linter.auxLemma false in
649651
theorem sizeOf_dedupKeys [SizeOf (Sigma β)]
650652
(xs : List (Sigma β)) : SizeOf.sizeOf (dedupKeys xs) ≤ SizeOf.sizeOf xs := by
651653
simp only [SizeOf.sizeOf, _sizeOf_1]

Mathlib/Init.lean

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public import Lean.LibrarySuggestions.Default -- for `+suggestions` modes in tac
55
public import Mathlib.Lean.Linter -- linter utilities; will be transitively imported in #31134
66
public import Mathlib.Tactic.AdaptationNote -- make #adaptation_note available everywhere
77
public import Mathlib.Tactic.Lemma
8+
public import Mathlib.Tactic.Linter.AuxLemma
89
public import Mathlib.Tactic.Linter.DeprecatedSyntaxLinter
910
public import Mathlib.Tactic.Linter.DirectoryDependency
1011
public import Mathlib.Tactic.Linter.DocPrime
@@ -84,6 +85,7 @@ all these linters, or add the `weak.linter.mathlibStandardSet` option to their l
8485
register_linter_set linter.mathlibStandardSet :=
8586
-- linter.allScriptsDocumented -- disabled, let's not impose this requirement downstream.
8687
-- linter.checkInitImports -- disabled, not relevant downstream.
88+
linter.auxLemma
8789
linter.flexible
8890
linter.hashCommand
8991
linter.oldObtain
@@ -128,7 +130,7 @@ register_linter_set linter.weeklyLintSet :=
128130
linter.tacticAnalysis.verifyGrindOnly
129131

130132
-- Check that all linter options mentioned in the mathlib standard linter set exist.
131-
open Lean Elab.Command Linter Mathlib.Linter Style UnusedInstancesInType
133+
open Lean Elab.Command Linter Mathlib.Linter Style UnusedInstancesInType AuxLemma
132134

133135
run_cmd liftTermElabM do
134136
let DefinedInScripts : Array Name :=

Mathlib/Tactic.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ public import Mathlib.Tactic.LinearCombination
160160
public import Mathlib.Tactic.LinearCombination.Lemmas
161161
public import Mathlib.Tactic.LinearCombinationPrime
162162
public import Mathlib.Tactic.Linter
163+
public import Mathlib.Tactic.Linter.AuxLemma
163164
public import Mathlib.Tactic.Linter.CommandRanges
164165
public import Mathlib.Tactic.Linter.CommandStart
165166
public import Mathlib.Tactic.Linter.DeprecatedModule
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/-
2+
Copyright (c) 2026 Kim Morrison. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Kim Morrison
5+
-/
6+
module
7+
8+
public meta import Lean.Elab.Command
9+
public meta import Mathlib.Tactic.Linter.Header -- shake: keep
10+
11+
/-!
12+
# The `auxLemma` linter
13+
14+
The `auxLemma` linter flags explicit references to auto-generated "auxiliary" declarations
15+
such as `_proof_1`, `match_1`, or `_sizeOf_1`.
16+
17+
## Why is this bad?
18+
19+
These names are internal implementation details generated by the Lean elaborator.
20+
They are not stable across refactors (e.g. reordering fields in a structure can renumber
21+
`_proof_` indices), so depending on them makes code fragile.
22+
-/
23+
24+
meta section
25+
26+
open Lean Elab Linter
27+
28+
namespace Mathlib.Linter.AuxLemma
29+
30+
/-- Returns `true` if `s` has the form `pfx ++ digits`, e.g. `"_proof_17"`. -/
31+
@[inline] private def matchesAuxPattern (pfx : String) (s : String) : Bool :=
32+
s.startsWith pfx && (s.drop pfx.length).all (·.isDigit) && s.rawEndPos > pfx.rawEndPos
33+
34+
/-- Returns `true` if `s` is an auto-generated auxiliary name component,
35+
such as `_proof_1`, `match_2`, or `_sizeOf_3`. -/
36+
private def isAuxName (s : String) : Bool :=
37+
matchesAuxPattern "_proof_" s ||
38+
matchesAuxPattern "match_" s ||
39+
matchesAuxPattern "_sizeOf_" s ||
40+
matchesAuxPattern "_simp_" s ||
41+
matchesAuxPattern "_aux_" s
42+
43+
/-- Returns `true` if any component of the name is an auto-generated auxiliary name. -/
44+
private def nameRefersToAuxLemma : Name → Bool
45+
| .str p s => isAuxName s || nameRefersToAuxLemma p
46+
| .num p _ => nameRefersToAuxLemma p
47+
| .anonymous => false
48+
49+
/-- The `auxLemma` linter emits a warning on any explicit reference to an auto-generated
50+
auxiliary declaration (such as `_proof_1`, `match_1`, or `_sizeOf_1`).
51+
52+
These names are internal to the Lean elaborator and are not stable across refactors. -/
53+
public register_option linter.auxLemma : Bool := {
54+
defValue := true
55+
descr := "enable the `auxLemma` linter"
56+
}
57+
58+
@[inherit_doc linter.auxLemma]
59+
def auxLemmaLinter : Linter where run := withSetOptionIn fun stx => do
60+
unless getLinterValue linter.auxLemma (← getLinterOptions) do
61+
return
62+
if ← MonadLog.hasErrors then
63+
return
64+
if let some id := stx.find? fun s => s.isIdent && nameRefersToAuxLemma s.getId then
65+
logLint linter.auxLemma id
66+
m!"`{id.getId}` refers to an auto-generated auxiliary declaration. \
67+
These are not stable across refactors; consider using a different approach."
68+
69+
initialize addLinter auxLemmaLinter
70+
71+
end Mathlib.Linter.AuxLemma

MathlibTest/Attribute/ToAdditive/Basic.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,11 +791,13 @@ def Subtype.mul_inv_iff_mul_inv {α β : Type} [Group α] [MyRing β] (a : α) (
791791
exists a
792792
simp
793793

794+
set_option linter.auxLemma false in
794795
/--
795796
info: Subtype.mul_inv_iff_mul_inv._proof_1 {α β : Type} [Group α] [MyRing β] (a : α) (b : β) : a * a⁻¹ = 1 ↔ b * b⁻¹ = 1
796797
-/
797798
#guard_msgs in
798799
#check Subtype.mul_inv_iff_mul_inv._proof_1
800+
set_option linter.auxLemma false in
799801
/--
800802
info: Subtype.add_neg_iff_mul_inv._proof_1 {α β : Type} [AddGroup α] [MyRing β] (a : α) (b : β) : a + -a = 0 ↔ b * b⁻¹ = 1
801803
-/

MathlibTest/Linter/AuxLemma.lean

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
module
2+
3+
public import Mathlib.Tactic.Linter.AuxLemma
4+
public import Mathlib.Init
5+
public import Init.Data.Iterators.Combinators.Monadic.FilterMap
6+
7+
/-!
8+
# Tests for the `auxLemma` linter
9+
10+
The important tests here reference *genuinely* auto-generated declarations (`match_1`,
11+
`_proof_1`, `_sizeOf_1`), produced by the elaborator from the definitions below. If Lean's
12+
naming scheme for these auxiliary declarations ever changes, these tests break — which is the
13+
point: the linter would otherwise silently stop catching anything.
14+
-/
15+
16+
set_option linter.auxLemma true
17+
18+
-- A `match`-based definition generates an auxiliary `foo.match_1`.
19+
def foo : Nat → Nat
20+
| 0 => 0
21+
| n + 1 => n
22+
23+
-- A structure with a proof field, plus an instance discharging it by tactic, generates
24+
-- `s._proof_1`; the structure itself generates `S._sizeOf_1`.
25+
structure S where
26+
n : Nat
27+
h : 0 < n
28+
29+
def s : S where
30+
n := 1
31+
h := by decide
32+
33+
/--
34+
warning: `foo.match_1` refers to an auto-generated auxiliary declaration. These are not stable across refactors; consider using a different approach.
35+
36+
Note: This linter can be disabled with `set_option linter.auxLemma false`
37+
-/
38+
#guard_msgs in
39+
example := @foo.match_1
40+
41+
/--
42+
warning: `s._proof_1` refers to an auto-generated auxiliary declaration. These are not stable across refactors; consider using a different approach.
43+
44+
Note: This linter can be disabled with `set_option linter.auxLemma false`
45+
-/
46+
#guard_msgs in
47+
example := s._proof_1
48+
49+
-- `_sizeOf_1` is a compiler-internal that cannot be used as a term value, so we reference it
50+
-- with `#check` and only guard the linter warning (ignoring the `#check` info output).
51+
/--
52+
warning: `S._sizeOf_1` refers to an auto-generated auxiliary declaration. These are not stable across refactors; consider using a different approach.
53+
54+
Note: This linter can be disabled with `set_option linter.auxLemma false`
55+
-/
56+
#guard_msgs(drop info, warning) in
57+
#check @S._sizeOf_1
58+
59+
-- No warning on ordinary references, including names that merely resemble the auxiliary
60+
-- patterns but lack the trailing digits (e.g. `Nat.rec`, `foo`).
61+
#guard_msgs in
62+
example := @foo
63+
64+
#guard_msgs in
65+
example := @Nat.rec
66+
67+
-- The linter can be turned off.
68+
set_option linter.auxLemma false in
69+
#guard_msgs in
70+
example := @foo.match_1
71+
72+
-- copied from `Std.Iterators.Types.FilterMap.instIterator` at the time of writing this test
73+
-- `_aux_1` refers to the first field of the instance `fooAux`
74+
open Std Iterators in
75+
universe w w' w'' in
76+
instance fooAux {α β γ : Type w} {m : Type w → Type w'} {n : Type w → Type w''} [Monad n]
77+
[Iterator α m β] {lift : ⦃α : Type w⦄ → m α → n α} {f : β → PostconditionT n γ} :
78+
Iterator (Types.Map α m n lift f) n γ :=
79+
inferInstanceAs <| Iterator (Types.FilterMap α m n lift _) n γ
80+
81+
/--
82+
warning: `fooAux._aux_1` refers to an auto-generated auxiliary declaration. These are not stable across refactors; consider using a different approach.
83+
84+
Note: This linter can be disabled with `set_option linter.auxLemma false`
85+
-/
86+
#guard_msgs(drop info,warning) in
87+
#check fooAux._aux_1
88+
89+
-- minimised from `BitVec.lt_of_msb_false_of_msb_true` at the time of writing this test
90+
structure Foo where
91+
data : Nat
92+
93+
def Foo.bar (_x : Foo) : Bool := false
94+
95+
instance : LT (Foo) := ⟨fun x y ↦ x.data < y.data⟩
96+
97+
axiom silentSorry {α} : α
98+
@[simp]
99+
theorem fooBar {x y : Foo} (_hx : x.bar = false) (_hy : y.bar = true) : x < y := silentSorry
100+
/--
101+
warning: `fooBar._simp_1` refers to an auto-generated auxiliary declaration. These are not stable across refactors; consider using a different approach.
102+
103+
Note: This linter can be disabled with `set_option linter.auxLemma false`
104+
-/
105+
#guard_msgs in
106+
example {x y : Foo} := fooBar._simp_1 (x := x) (y := y)

MathlibTest/Util/CompileInductive.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ example := @Nat.brecOn
2121
example := @List.brecOn
2222
example := @Fin2.brecOn
2323

24+
set_option linter.auxLemma false in
2425
example := @List._sizeOf_1
2526

2627
open Lean Elab Term

0 commit comments

Comments
 (0)