Skip to content

Commit ccc281f

Browse files
grunwegadomani
andcommitted
feat: rewrite the duplicateImports linter (#17555)
as part of the header linter. This yields better error spans, is more robust (as it avoids false positives from text-based parsing) and yields results more quickly (providing a better user experience). Also rewrite the `broadImports` linter to log warnings directly, as opposed to passing them down to the `headerLinter` and logging them there. Co-authored-by: grunweg <rothgami@math.hu-berlin.de> Co-authored-by: adomani <adomani@gmail.com>
1 parent dd71ace commit ccc281f

File tree

4 files changed

+54
-72
lines changed

4 files changed

+54
-72
lines changed

Mathlib/Tactic/Linter/Header.lean

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -205,36 +205,6 @@ def copyrightHeaderChecks (copyright : String) : Array (Syntax × String) := Id.
205205
output := output.push (toSyntax copyright "-/", s!"Copyright too short!")
206206
return output
207207

208-
/-- Check the `Syntax` `imports` for broad imports: either `Mathlib.Tactic` or any import
209-
starting with `Lake`. -/
210-
def broadImportsCheck (imports : Array Syntax) (mainModule : Name) : Array (Syntax × String) := Id.run do
211-
let mut output := #[]
212-
for i in imports do
213-
match i.getId with
214-
| `Mathlib.Tactic =>
215-
output := output.push (i, s!"Files in mathlib cannot import the whole tactic folder.")
216-
| `Mathlib.Tactic.Replace =>
217-
if mainModule != `Mathlib.Tactic then output := output.push (i,
218-
s!"Mathlib.Tactic.Replace defines a deprecated form of the 'replace' tactic; \
219-
please do not use it in mathlib.")
220-
| `Mathlib.Tactic.Have =>
221-
if ![`Mathlib.Tactic, `Mathlib.Tactic.Replace].contains mainModule then
222-
output := output.push (i,
223-
s!"Mathlib.Tactic.Have defines a deprecated form of the 'have' tactic; \
224-
please do not use it in mathlib.")
225-
| modName =>
226-
if modName.getRoot == `Lake then
227-
output := output.push (i,
228-
s!"In the past, importing 'Lake' in mathlib has led to dramatic slow-downs of the linter \
229-
(see e.g. mathlib4#13779). Please consider carefully if this import is useful and \
230-
make sure to benchmark it. If this is fine, feel free to allow this linter.")
231-
else if (`Mathlib.Deprecated).isPrefixOf modName &&
232-
!(`Mathlib.Deprecated).isPrefixOf mainModule then
233-
-- We do not complain about files in the `Deprecated` directory importing one another.
234-
output := output.push (i, s!"Files in the `Deprecated` directory are not supposed to be imported.")
235-
236-
return output
237-
238208
/--
239209
The "header" style linter checks that a file starts with
240210
```
@@ -263,6 +233,47 @@ register_option linter.style.header : Bool := {
263233

264234
namespace Style.header
265235

236+
/-- Check the `Syntax` `imports` for broad imports:
237+
`Mathlib.Tactic`, any import starting with `Lake`, `Mathlib.Tactic.{Have,Replace}`
238+
or anything in the `Deprecated` folder. -/
239+
def broadImportsCheck (imports : Array Syntax) (mainModule : Name) : CommandElabM Unit := do
240+
for i in imports do
241+
match i.getId with
242+
| `Mathlib.Tactic =>
243+
Linter.logLint linter.style.header i "Files in mathlib cannot import the whole tactic folder."
244+
| `Mathlib.Tactic.Replace =>
245+
if mainModule != `Mathlib.Tactic then
246+
Linter.logLint linter.style.header i
247+
"'Mathlib.Tactic.Replace' defines a deprecated form of the 'replace' tactic; \
248+
please do not use it in mathlib."
249+
| `Mathlib.Tactic.Have =>
250+
if ![`Mathlib.Tactic, `Mathlib.Tactic.Replace].contains mainModule then
251+
Linter.logLint linter.style.header i
252+
"'Mathlib.Tactic.Have' defines a deprecated form of the 'have' tactic; \
253+
please do not use it in mathlib."
254+
| modName =>
255+
if modName.getRoot == `Lake then
256+
Linter.logLint linter.style.header i
257+
"In the past, importing 'Lake' in mathlib has led to dramatic slow-downs of the linter \
258+
(see e.g. mathlib4#13779). Please consider carefully if this import is useful and \
259+
make sure to benchmark it. If this is fine, feel free to silence this linter."
260+
else if (`Mathlib.Deprecated).isPrefixOf modName &&
261+
!(`Mathlib.Deprecated).isPrefixOf mainModule then
262+
-- We do not complain about files in the `Deprecated` directory importing one another.
263+
Linter.logLint linter.style.header i
264+
"Files in the `Deprecated` directory are not supposed to be imported."
265+
266+
/-- Check the syntax `imports` for syntactically duplicate imports.
267+
The output is an array of `Syntax` atoms whose ranges are the import statements,
268+
and the embedded strings are the error message of the linter.
269+
-/
270+
def duplicateImportsCheck (imports : Array Syntax) : CommandElabM Unit := do
271+
let mut importsSoFar := #[]
272+
for i in imports do
273+
if importsSoFar.contains i then
274+
Linter.logLint linter.style.header i m!"Duplicate imports: '{i}' already imported"
275+
else importsSoFar := importsSoFar.push i
276+
266277
@[inherit_doc Mathlib.Linter.linter.style.header]
267278
def headerLinter : Linter where run := withSetOptionIn fun stx ↦ do
268279
unless Linter.getLinterValue linter.style.header (← getOptions) do
@@ -290,9 +301,10 @@ def headerLinter : Linter where run := withSetOptionIn fun stx ↦ do
290301
let (stx, _) ← Parser.parseHeader { input := fm.source, fileName := fil, fileMap := fm }
291302
parseUpToHere (stx.getTailPos?.getD default) "\nsection")
292303
let importIds := getImportIds upToStx
293-
-- Report on broad imports.
294-
for (imp, msg) in broadImportsCheck importIds mainModule do
295-
Linter.logLint linter.style.header imp msg
304+
-- Report on broad or duplicate imports.
305+
broadImportsCheck importIds mainModule
306+
duplicateImportsCheck importIds
307+
296308
let afterImports := firstNonImport? upToStx
297309
if afterImports.isNone then return
298310
let copyright := match upToStx.getHeadInfo with

Mathlib/Tactic/Linter/TextBased.lean

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ Authors: Michael Rothgang
66

77
import Batteries.Data.String.Matcher
88
import Mathlib.Data.Nat.Notation
9-
import Std.Data.HashMap.Basic
109

1110
/-!
1211
## Text-based linters
@@ -57,7 +56,6 @@ inductive StyleError where
5756
| adaptationNote
5857
/-- A line ends with windows line endings (\r\n) instead of unix ones (\n). -/
5958
| windowsLineEnding
60-
| duplicateImport (importStatement: String) (alreadyImportedLine: ℕ)
6159
deriving BEq
6260

6361
/-- How to format style errors -/
@@ -78,16 +76,13 @@ def StyleError.errorMessage (err : StyleError) : String := match err with
7876
"Found the string \"Adaptation note:\", please use the #adaptation_note command instead"
7977
| windowsLineEnding => "This line ends with a windows line ending (\r\n): please use Unix line\
8078
endings (\n) instead"
81-
| StyleError.duplicateImport (importStatement) (alreadyImportedLine) =>
82-
s!"Duplicate imports: {importStatement} (already imported on line {alreadyImportedLine})"
8379

8480
/-- The error code for a given style error. Keep this in sync with `parse?_errorContext` below! -/
8581
-- FUTURE: we're matching the old codes in `lint-style.py` for compatibility;
8682
-- in principle, we could also print something more readable.
8783
def StyleError.errorCode (err : StyleError) : String := match err with
8884
| StyleError.adaptationNote => "ERR_ADN"
8985
| StyleError.windowsLineEnding => "ERR_WIN"
90-
| StyleError.duplicateImport _ _ => "ERR_DIMP"
9186

9287
/-- Context for a style error: the actual error, the line number in the file we're reading
9388
and the path to the file. -/
@@ -122,12 +117,8 @@ def compare (existing new : ErrorContext) : ComparisonResult :=
122117

123118
-- NB: keep the following in sync with `parse?_errorContext` below.
124119
-- Generally, comparable errors must have equal `StyleError`s.
125-
else match (existing.error, new.error) with
126-
-- We do *not* care about the kind or line number of a duplicate import.
127-
| (StyleError.duplicateImport _ _, StyleError.duplicateImport _ _) =>
128-
ComparisonResult.Comparable
129-
-- In all other cases, `StyleErrors` must compare equal.
130-
| (a, b) => if a == b then ComparisonResult.Comparable else ComparisonResult.Different
120+
else
121+
if existing.error == new.error then ComparisonResult.Comparable else ComparisonResult.Different
131122

132123
/-- Find the first style exception in `exceptions` (if any) which covers a style exception `e`. -/
133124
def ErrorContext.find?_comparable (e : ErrorContext) (exceptions : Array ErrorContext) :
@@ -170,7 +161,6 @@ def parse?_errorContext (line : String) : Option ErrorContext := Id.run do
170161
-- NB: keep this in sync with `compare` above!
171162
| "ERR_ADN" => some (StyleError.adaptationNote)
172163
| "ERR_WIN" => some (StyleError.windowsLineEnding)
173-
| "ERR_DIMP" => some (StyleError.duplicateImport "" 0)
174164
| _ => none
175165
match String.toNat? lineNumber with
176166
| some n => err.map fun e ↦ (ErrorContext.mk e n path)
@@ -214,26 +204,6 @@ def adaptationNoteLinter : TextbasedLinter := fun lines ↦ Id.run do
214204
lineNumber := lineNumber + 1
215205
return (errors, none)
216206

217-
/-- Lint on a collection of input strings if one of the is a duplicate import statement. -/
218-
def duplicateImportsLinter : TextbasedLinter := fun lines ↦ Id.run do
219-
let mut lineNumber := 1
220-
let mut errors := Array.mkEmpty 0
221-
let mut importStatements : Std.HashMap String ℕ := {}
222-
for line in lines do
223-
if line.startsWith "import " then
224-
let lineWithoutComment := (line.splitOn "--")[0]!
225-
let importStatement := lineWithoutComment.trim
226-
if importStatements.contains importStatement then
227-
let alreadyImportedLine := importStatements[importStatement]!
228-
errors := errors.push (
229-
(StyleError.duplicateImport importStatement alreadyImportedLine),
230-
lineNumber
231-
)
232-
else
233-
importStatements := importStatements.insert importStatement lineNumber
234-
lineNumber := lineNumber + 1
235-
return (errors, none)
236-
237207
/-- Whether a collection of lines consists *only* of imports, blank lines and single-line comments.
238208
In practice, this means it's an imports-only file and exempt from almost all linting. -/
239209
def isImportsOnlyFile (lines : Array String) : Bool :=
@@ -245,7 +215,7 @@ end
245215

246216
/-- All text-based linters registered in this file. -/
247217
def allLinters : Array TextbasedLinter := #[
248-
adaptationNoteLinter, duplicateImportsLinter
218+
adaptationNoteLinter
249219
]
250220

251221

MathlibTest/Header.lean

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,27 @@ Authors: Damiano Testa
66

77
import Mathlib.Tactic.Linter.Header
88
import Lake
9+
import Mathlib.Tactic.Linter.Header
910
import /- -/ Mathlib.Tactic -- the `TextBased` linter does not flag this `broadImport`
1011
import Mathlib.Tactic.Have
1112
import Mathlib.Deprecated.Subfield
1213

1314
/--
14-
warning: In the past, importing 'Lake' in mathlib has led to dramatic slow-downs of the linter (see e.g. mathlib4#13779). Please consider carefully if this import is useful and make sure to benchmark it. If this is fine, feel free to allow this linter.
15+
warning: In the past, importing 'Lake' in mathlib has led to dramatic slow-downs of the linter (see e.g. mathlib4#13779). Please consider carefully if this import is useful and make sure to benchmark it. If this is fine, feel free to silence this linter.
1516
note: this linter can be disabled with `set_option linter.style.header false`
1617
---
1718
warning: Files in mathlib cannot import the whole tactic folder.
1819
note: this linter can be disabled with `set_option linter.style.header false`
1920
---
20-
warning: Mathlib.Tactic.Have defines a deprecated form of the 'have' tactic; please do not use it in mathlib.
21+
warning: 'Mathlib.Tactic.Have' defines a deprecated form of the 'have' tactic; please do not use it in mathlib.
2122
note: this linter can be disabled with `set_option linter.style.header false`
2223
---
2324
warning: Files in the `Deprecated` directory are not supposed to be imported.
2425
note: this linter can be disabled with `set_option linter.style.header false`
2526
---
27+
warning: Duplicate imports: 'Mathlib.Tactic.Linter.Header' already imported
28+
note: this linter can be disabled with `set_option linter.style.header false`
29+
---
2630
warning: The module doc-string for a file should be the first command after the imports.
2731
Please, add a module doc-string before `/-!# Tests for the `docModule` linter
2832
-/

scripts/nolints-style.txt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,3 @@ Mathlib/Tactic/Linter/TextBased.lean : line 84 : ERR_ADN : Found the string "Ada
1515
Mathlib/Tactic/Linter/TextBased.lean : line 274 : ERR_ADN : Found the string "Adaptation note:", please use the #adaptation_note command instead
1616
Mathlib/Tactic/Linter/TextBased.lean : line 279 : ERR_ADN : Found the string "Adaptation note:", please use the #adaptation_note command instead
1717
Mathlib/Tactic/Linter/TextBased.lean : line 280 : ERR_ADN : Found the string "Adaptation note:", please use the #adaptation_note command instead
18-
19-
-- The "duplicate imports" linter fires on this line inside a multi-line module docstring,
20-
-- which says 'import statements': this is a false positive.
21-
Mathlib/Tactic/Linter/Header.lean : line 228 : ERR_DIMP : Duplicate imports: import statements* (already imported on line 19)

0 commit comments

Comments
 (0)