Skip to content

Commit 82d756e

Browse files
thorimurJovanGerbbryangingechen
committed
feat: privateModule linter (#31820)
This linter lints against nonempty modules that have only private declarations, and suggests adding `@[expose] public section` to the top. This linter found 1 violation in Mathlib, which was fixed in #31822. Co-authored-by: thorimur <thomasmurrills@gmail.com> Co-authored-by: Jovan Gerbscheid <56355248+JovanGerb@users.noreply.github.com> Co-authored-by: Bryan Gin-ge Chen <5209952+bryangingechen@users.noreply.github.com>
1 parent d5c9558 commit 82d756e

File tree

9 files changed

+132
-0
lines changed

9 files changed

+132
-0
lines changed

Mathlib.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6450,6 +6450,7 @@ public import Mathlib.Tactic.Linter.MinImports
64506450
public import Mathlib.Tactic.Linter.Multigoal
64516451
public import Mathlib.Tactic.Linter.OldObtain
64526452
public import Mathlib.Tactic.Linter.PPRoundtrip
6453+
public import Mathlib.Tactic.Linter.PrivateModule
64536454
public import Mathlib.Tactic.Linter.Style
64546455
public import Mathlib.Tactic.Linter.TextBased
64556456
public import Mathlib.Tactic.Linter.UnusedTactic

Mathlib/Init.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public import Mathlib.Tactic.Linter.FlexibleLinter
1616
public import Mathlib.Tactic.Linter.Lint
1717
public import Mathlib.Tactic.Linter.Multigoal
1818
public import Mathlib.Tactic.Linter.OldObtain
19+
public import Mathlib.Tactic.Linter.PrivateModule
1920
-- The following import contains the environment extension for the unused tactic linter.
2021
public import Mathlib.Tactic.Linter.UnusedTacticExtension
2122
public import Mathlib.Tactic.Linter.UnusedTactic
@@ -67,6 +68,7 @@ register_linter_set linter.mathlibStandardSet :=
6768
-- linter.checkInitImports -- disabled, not relevant downstream.
6869
linter.hashCommand
6970
linter.oldObtain
71+
linter.privateModule
7072
linter.style.cases
7173
linter.style.induction
7274
linter.style.refine

Mathlib/Tactic.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ public import Mathlib.Tactic.Linter.MinImports
171171
public import Mathlib.Tactic.Linter.Multigoal
172172
public import Mathlib.Tactic.Linter.OldObtain
173173
public import Mathlib.Tactic.Linter.PPRoundtrip
174+
public import Mathlib.Tactic.Linter.PrivateModule
174175
public import Mathlib.Tactic.Linter.Style
175176
public import Mathlib.Tactic.Linter.TextBased
176177
public import Mathlib.Tactic.Linter.UnusedTactic

Mathlib/Tactic/Linter.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ public meta import Mathlib.Tactic.Linter.DeprecatedModule
1313
public meta import Mathlib.Tactic.Linter.HaveLetLinter
1414
public meta import Mathlib.Tactic.Linter.MinImports
1515
public meta import Mathlib.Tactic.Linter.PPRoundtrip
16+
public meta import Mathlib.Tactic.Linter.PrivateModule
1617
public meta import Mathlib.Tactic.Linter.UpstreamableDecl
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/-
2+
Copyright (c) 2025 Thomas R. Murrills. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Thomas R. Murrills
5+
-/
6+
module
7+
8+
public meta import Lean.Elab.Command
9+
public import Lean.Linter.Basic
10+
public import Lean.Environment
11+
-- Import this linter explicitly to ensure that
12+
-- this file has a valid copyright header and module docstring.
13+
import Mathlib.Tactic.Linter.Header
14+
15+
/-!
16+
# Private module linter
17+
18+
This linter lints against nonempty modules that have only private declarations, and suggests adding
19+
`@[expose] public section` to the top or selectively marking declarations as `public`.
20+
21+
## Implementation notes
22+
23+
`env.constants.map₂` contains all locally-defined constants, and accessing this waits until all
24+
declarations are added. By linting (only) the `eoi` token, we can capture all constants defined in
25+
the file.
26+
27+
Note that private declarations are exactly those which satisfy `isPrivateName`, whether private due
28+
to an explicit `private` or due to not being made `public`.
29+
-/
30+
31+
meta section
32+
33+
open Lean Elab Command Linter
34+
35+
namespace Mathlib.Linter
36+
37+
/-- The `privateModule` linter lints against nonempty modules that have only private declarations,
38+
and suggests adding `@[expose] public section` or selectively marking declarations as `public`. -/
39+
public register_option linter.privateModule : Bool := {
40+
defValue := false
41+
descr := "Enable the `privateModule` linter, which lints against nonempty modules that have only \
42+
private declarations."
43+
}
44+
45+
/--
46+
The `privateModule` linter lints against nonempty modules that have only private declarations,
47+
and suggests adding `@[expose] public section` to the top.
48+
49+
This linter only acts on the end-of-input `Parser.Command.eoi` token, and ignores all other syntax.
50+
It logs its message at the top of the file.
51+
-/
52+
def privateModule : Linter where run stx := do
53+
if stx.isOfKind ``Parser.Command.eoi then
54+
unless getLinterValue linter.privateModule (← getLinterOptions) do
55+
return
56+
if (← getEnv).header.isModule then
57+
-- Don't lint an imports-only module:
58+
if !(← getEnv).constants.map₂.isEmpty then
59+
-- Exit if any declaration is public:
60+
for (decl, _) in (← getEnv).constants.map₂ do
61+
if !isPrivateName decl then return
62+
-- Lint if all names are private:
63+
let topOfFileRef := Syntax.atom (.synthetic ⟨0⟩ ⟨0⟩) ""
64+
logLint linter.privateModule topOfFileRef
65+
"The current module only contains private declarations.\n\n\
66+
Consider adding `@[expose] public section` at the beginning of the module, \
67+
or selectively marking declarations as `public`."
68+
69+
initialize addLinter privateModule
70+
71+
end Mathlib.Linter
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module
2+
3+
import Mathlib.Init
4+
import all Mathlib.Tactic.Linter.PrivateModule
5+
import Lean.Elab.Command
6+
7+
open Lean
8+
9+
set_option linter.mathlibStandardSet true
10+
11+
theorem foo : True := trivial
12+
13+
def bar : Bool := true
14+
15+
-- Run the linter on artificial `eoi` syntax so that we can actually guard the message
16+
open Mathlib.Linter Parser in
17+
/--
18+
warning: The current module only contains private declarations.
19+
20+
Consider adding `@[expose] public section` at the beginning of the module, or selectively marking declarations as `public`.
21+
22+
Note: This linter can be disabled with `set_option linter.privateModule false`
23+
-/
24+
#guard_msgs in
25+
run_cmd do
26+
let eoi := mkNode ``Command.eoi #[mkAtom .none ""]
27+
privateModule.run eoi
28+
29+
-- Disable so that this test is silent
30+
set_option linter.privateModule false
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module
2+
3+
import Mathlib.Tactic.Linter.PrivateModule
4+
5+
set_option linter.privateModule true
6+
7+
-- Should not fire, since `foo` is `public`.
8+
9+
public theorem foo : True := trivial
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module
2+
3+
import Mathlib.Tactic.Linter.PrivateModule
4+
import Mathlib.Logic.Basic
5+
6+
set_option linter.privateModule true
7+
8+
/- Should not fire, since the file has no declarations. -/
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Mathlib.Tactic.Linter.PrivateModule
2+
3+
set_option linter.privateModule true
4+
5+
/- Should not fire because this file does not use `module`, even though it is nonempty and has only private defs. -/
6+
7+
private theorem foo : True := trivial
8+
9+
private def bar : Bool := true

0 commit comments

Comments
 (0)