Skip to content

Commit 671c090

Browse files
committed
chore: move style linters to Style (#19529)
This makes for a more coherent split of the linter files. Third attempt after #15942 and #16510.
1 parent 6b724bf commit 671c090

File tree

5 files changed

+573
-560
lines changed

5 files changed

+573
-560
lines changed

Mathlib/Tactic/Linter/Lint.lean

Lines changed: 3 additions & 334 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import Mathlib.Tactic.DeclarationNames
99
/-!
1010
# Linters for Mathlib
1111
12-
In this file we define additional linters for mathlib.
12+
In this file we define additional linters for mathlib,
13+
which concern the *behaviour* of the linted code, and not issues of code style or formatting.
1314
1415
Perhaps these should be moved to Batteries in the future.
1516
-/
@@ -95,336 +96,4 @@ def dupNamespace : Linter where run := withSetOptionIn fun stx ↦ do
9596

9697
initialize addLinter dupNamespace
9798

98-
end DupNamespaceLinter
99-
100-
/-!
101-
# The "missing end" linter
102-
103-
The "missing end" linter emits a warning on non-closed `section`s and `namespace`s.
104-
It allows the "outermost" `noncomputable section` to be left open (whether or not it is named).
105-
-/
106-
107-
open Lean Elab Command
108-
109-
/-- The "missing end" linter emits a warning on non-closed `section`s and `namespace`s.
110-
It allows the "outermost" `noncomputable section` to be left open (whether or not it is named).
111-
-/
112-
register_option linter.style.missingEnd : Bool := {
113-
defValue := false
114-
descr := "enable the missing end linter"
115-
}
116-
117-
namespace Style.missingEnd
118-
119-
@[inherit_doc Mathlib.Linter.linter.style.missingEnd]
120-
def missingEndLinter : Linter where run := withSetOptionIn fun stx ↦ do
121-
-- Only run this linter at the end of a module.
122-
unless stx.isOfKind ``Lean.Parser.Command.eoi do return
123-
if Linter.getLinterValue linter.style.missingEnd (← getOptions) &&
124-
!(← MonadState.get).messages.hasErrors then
125-
let sc ← getScopes
126-
-- The last scope is always the "base scope", corresponding to no active `section`s or
127-
-- `namespace`s. We are interested in any *other* unclosed scopes.
128-
if sc.length == 1 then return
129-
let ends := sc.dropLast.map fun s ↦ (s.header, s.isNoncomputable)
130-
-- If the outermost scope corresponds to a `noncomputable section`, we ignore it.
131-
let ends := if ends.getLast!.2 then ends.dropLast else ends
132-
-- If there are any further un-closed scopes, we emit a warning.
133-
if !ends.isEmpty then
134-
let ending := (ends.map Prod.fst).foldl (init := "") fun a b ↦
135-
a ++ s!"\n\nend{if b == "" then "" else " "}{b}"
136-
Linter.logLint linter.style.missingEnd stx
137-
m!"unclosed sections or namespaces; expected: '{ending}'"
138-
139-
initialize addLinter missingEndLinter
140-
141-
end Style.missingEnd
142-
143-
/-!
144-
# The `cdot` linter
145-
146-
The `cdot` linter is a syntax-linter that flags uses of the "cdot" `·` that are achieved
147-
by typing a character different from `·`.
148-
For instance, a "plain" dot `.` is allowed syntax, but is flagged by the linter.
149-
It also flags "isolated cdots", i.e. when the `·` is on its own line.
150-
-/
151-
152-
/--
153-
The `cdot` linter flags uses of the "cdot" `·` that are achieved by typing a character
154-
different from `·`.
155-
For instance, a "plain" dot `.` is allowed syntax, but is flagged by the linter.
156-
It also flags "isolated cdots", i.e. when the `·` is on its own line. -/
157-
register_option linter.style.cdot : Bool := {
158-
defValue := false
159-
descr := "enable the `cdot` linter"
160-
}
161-
162-
/-- `isCDot? stx` checks whether `stx` is a `Syntax` node corresponding to a `cdot` typed with
163-
the character `·`. -/
164-
def isCDot? : Syntax → Bool
165-
| .node _ ``cdotTk #[.node _ `patternIgnore #[.node _ _ #[.atom _ v]]] => v == "·"
166-
| .node _ ``Lean.Parser.Term.cdot #[.atom _ v] => v == "·"
167-
| _ => false
168-
169-
/--
170-
`findCDot stx` extracts from `stx` the syntax nodes of `kind` `Lean.Parser.Term.cdot` or `cdotTk`. -/
171-
partial
172-
def findCDot : Syntax → Array Syntax
173-
| stx@(.node _ kind args) =>
174-
let dargs := (args.map findCDot).flatten
175-
match kind with
176-
| ``Lean.Parser.Term.cdot | ``cdotTk => dargs.push stx
177-
| _ => dargs
178-
|_ => #[]
179-
180-
/-- `unwanted_cdot stx` returns an array of syntax atoms within `stx`
181-
corresponding to `cdot`s that are not written with the character `·`.
182-
This is precisely what the `cdot` linter flags.
183-
-/
184-
def unwanted_cdot (stx : Syntax) : Array Syntax :=
185-
(findCDot stx).filter (!isCDot? ·)
186-
187-
namespace Style
188-
189-
@[inherit_doc linter.style.cdot]
190-
def cdotLinter : Linter where run := withSetOptionIn fun stx ↦ do
191-
unless Linter.getLinterValue linter.style.cdot (← getOptions) do
192-
return
193-
if (← MonadState.get).messages.hasErrors then
194-
return
195-
for s in unwanted_cdot stx do
196-
Linter.logLint linter.style.cdot s
197-
m!"Please, use '·' (typed as `\\.`) instead of '{s}' as 'cdot'."
198-
-- We also check for isolated cdot's, i.e. when the cdot is on its own line.
199-
for cdot in Mathlib.Linter.findCDot stx do
200-
match cdot.find? (·.isOfKind `token.«· ») with
201-
| some (.node _ _ #[.atom (.original _ _ afterCDot _) _]) =>
202-
if (afterCDot.takeWhile (·.isWhitespace)).contains '\n' then
203-
logWarningAt cdot <| .tagged linter.style.cdot.name
204-
m!"This central dot `·` is isolated; please merge it with the next line."
205-
| _ => return
206-
207-
initialize addLinter cdotLinter
208-
209-
end Style
210-
211-
/-!
212-
# The `dollarSyntax` linter
213-
214-
The `dollarSyntax` linter flags uses of `<|` that are achieved by typing `$`.
215-
These are disallowed by the mathlib style guide, as using `<|` pairs better with `|>`.
216-
-/
217-
218-
/-- The `dollarSyntax` linter flags uses of `<|` that are achieved by typing `$`.
219-
These are disallowed by the mathlib style guide, as using `<|` pairs better with `|>`. -/
220-
register_option linter.style.dollarSyntax : Bool := {
221-
defValue := false
222-
descr := "enable the `dollarSyntax` linter"
223-
}
224-
225-
namespace Style.dollarSyntax
226-
227-
/-- `findDollarSyntax stx` extracts from `stx` the syntax nodes of `kind` `$`. -/
228-
partial
229-
def findDollarSyntax : Syntax → Array Syntax
230-
| stx@(.node _ kind args) =>
231-
let dargs := (args.map findDollarSyntax).flatten
232-
match kind with
233-
| ``«term_$__» => dargs.push stx
234-
| _ => dargs
235-
|_ => #[]
236-
237-
@[inherit_doc linter.style.dollarSyntax]
238-
def dollarSyntaxLinter : Linter where run := withSetOptionIn fun stx ↦ do
239-
unless Linter.getLinterValue linter.style.dollarSyntax (← getOptions) do
240-
return
241-
if (← MonadState.get).messages.hasErrors then
242-
return
243-
for s in findDollarSyntax stx do
244-
Linter.logLint linter.style.dollarSyntax s
245-
m!"Please use '<|' instead of '$' for the pipe operator."
246-
247-
initialize addLinter dollarSyntaxLinter
248-
249-
end Style.dollarSyntax
250-
251-
/-!
252-
# The `lambdaSyntax` linter
253-
254-
The `lambdaSyntax` linter is a syntax linter that flags uses of the symbol `λ` to define anonymous
255-
functions, as opposed to the `fun` keyword. These are syntactically equivalent; mathlib style
256-
prefers the latter as it is considered more readable.
257-
-/
258-
259-
/--
260-
The `lambdaSyntax` linter flags uses of the symbol `λ` to define anonymous functions.
261-
This is syntactically equivalent to the `fun` keyword; mathlib style prefers using the latter.
262-
-/
263-
register_option linter.style.lambdaSyntax : Bool := {
264-
defValue := false
265-
descr := "enable the `lambdaSyntax` linter"
266-
}
267-
268-
namespace Style.lambdaSyntax
269-
270-
/--
271-
`findLambdaSyntax stx` extracts from `stx` all syntax nodes of `kind` `Term.fun`. -/
272-
partial
273-
def findLambdaSyntax : Syntax → Array Syntax
274-
| stx@(.node _ kind args) =>
275-
let dargs := (args.map findLambdaSyntax).flatten
276-
match kind with
277-
| ``Parser.Term.fun => dargs.push stx
278-
| _ => dargs
279-
|_ => #[]
280-
281-
@[inherit_doc linter.style.lambdaSyntax]
282-
def lambdaSyntaxLinter : Linter where run := withSetOptionIn fun stx ↦ do
283-
unless Linter.getLinterValue linter.style.lambdaSyntax (← getOptions) do
284-
return
285-
if (← MonadState.get).messages.hasErrors then
286-
return
287-
for s in findLambdaSyntax stx do
288-
if let .atom _ "λ" := s[0] then
289-
Linter.logLint linter.style.lambdaSyntax s[0] m!"\
290-
Please use 'fun' and not 'λ' to define anonymous functions.\n\
291-
The 'λ' syntax is deprecated in mathlib4."
292-
293-
initialize addLinter lambdaSyntaxLinter
294-
295-
end Style.lambdaSyntax
296-
297-
/-!
298-
# The "longFile" linter
299-
300-
The "longFile" linter emits a warning on files which are longer than a certain number of lines
301-
(1500 by default).
302-
-/
303-
304-
/--
305-
The "longFile" linter emits a warning on files which are longer than a certain number of lines
306-
(`linter.style.longFileDefValue` by default on mathlib, no limit for downstream projects).
307-
If this option is set to `N` lines, the linter warns once a file has more than `N` lines.
308-
A value of `0` silences the linter entirely.
309-
-/
310-
register_option linter.style.longFile : Nat := {
311-
defValue := 0
312-
descr := "enable the longFile linter"
313-
}
314-
315-
/-- The number of lines that the `longFile` linter considers the default. -/
316-
register_option linter.style.longFileDefValue : Nat := {
317-
defValue := 1500
318-
descr := "a soft upper bound on the number of lines of each file"
319-
}
320-
321-
namespace Style.longFile
322-
323-
@[inherit_doc Mathlib.Linter.linter.style.longFile]
324-
def longFileLinter : Linter where run := withSetOptionIn fun stx ↦ do
325-
let linterBound := linter.style.longFile.get (← getOptions)
326-
if linterBound == 0 then
327-
return
328-
let defValue := linter.style.longFileDefValue.get (← getOptions)
329-
let smallOption := match stx with
330-
| `(set_option linter.style.longFile $x) => TSyntax.getNat ⟨x.raw⟩ ≤ defValue
331-
| _ => false
332-
if smallOption then
333-
logWarningAt stx <| .tagged linter.style.longFile.name
334-
m!"The default value of the `longFile` linter is {defValue}.\n\
335-
The current value of {linterBound} does not exceed the allowed bound.\n\
336-
Please, remove the `set_option linter.style.longFile {linterBound}`."
337-
else
338-
-- Thanks to the above check, the linter option is either not set (and hence equal
339-
-- to the default) or set to some value *larger* than the default.
340-
-- `Parser.isTerminalCommand` allows `stx` to be `#exit`: this is useful for tests.
341-
unless Parser.isTerminalCommand stx do return
342-
-- We exclude `Mathlib.lean` from the linter: it exceeds linter's default number of allowed
343-
-- lines, and it is an auto-generated import-only file.
344-
-- TODO: if there are more such files, revise the implementation.
345-
if (← getMainModule) == `Mathlib then return
346-
if let some init := stx.getTailPos? then
347-
-- the last line: we subtract 1, since the last line is expected to be empty
348-
let lastLine := ((← getFileMap).toPosition init).line
349-
-- In this case, the file has an allowed length, and the linter option is unnecessarily set.
350-
if lastLine ≤ defValue && defValue < linterBound then
351-
logWarningAt stx <| .tagged linter.style.longFile.name
352-
m!"The default value of the `longFile` linter is {defValue}.\n\
353-
This file is {lastLine} lines long which does not exceed the allowed bound.\n\
354-
Please, remove the `set_option linter.style.longFile {linterBound}`."
355-
else
356-
-- `candidate` is divisible by `100` and satisfies `lastLine + 100 < candidate ≤ lastLine + 200`
357-
-- note that either `lastLine ≤ defValue` and `defValue = linterBound` hold or
358-
-- `candidate` is necessarily bigger than `lastLine` and hence bigger than `defValue`
359-
let candidate := (lastLine / 100) * 100 + 200
360-
let candidate := max candidate defValue
361-
-- In this case, the file is longer than the default and also than what the option says.
362-
if defValue ≤ linterBound && linterBound < lastLine then
363-
logWarningAt stx <| .tagged linter.style.longFile.name
364-
m!"This file is {lastLine} lines long, but the limit is {linterBound}.\n\n\
365-
You can extend the allowed length of the file using \
366-
`set_option linter.style.longFile {candidate}`.\n\
367-
You can completely disable this linter by setting the length limit to `0`."
368-
else
369-
-- Finally, the file exceeds the default value, but not the option: we only allow the value
370-
-- of the option to be `candidate` or `candidate + 100`.
371-
-- In particular, this flags any option that is set to an unnecessarily high value.
372-
if linterBound == candidate || linterBound + 100 == candidate then return
373-
else
374-
logWarningAt stx <| .tagged linter.style.longFile.name
375-
m!"This file is {lastLine} lines long. \
376-
The current limit is {linterBound}, but it is expected to be {candidate}:\n\
377-
`set_option linter.style.longFile {candidate}`."
378-
379-
initialize addLinter longFileLinter
380-
381-
end Style.longFile
382-
383-
/-! # The "longLine linter" -/
384-
385-
/-- The "longLine" linter emits a warning on lines longer than 100 characters.
386-
We allow lines containing URLs to be longer, though. -/
387-
register_option linter.style.longLine : Bool := {
388-
defValue := false
389-
descr := "enable the longLine linter"
390-
}
391-
392-
namespace Style.longLine
393-
394-
@[inherit_doc Mathlib.Linter.linter.style.longLine]
395-
def longLineLinter : Linter where run := withSetOptionIn fun stx ↦ do
396-
unless Linter.getLinterValue linter.style.longLine (← getOptions) do
397-
return
398-
if (← MonadState.get).messages.hasErrors then
399-
return
400-
-- The linter ignores the `#guard_msgs` command, in particular its doc-string.
401-
-- The linter still lints the message guarded by `#guard_msgs`.
402-
if stx.isOfKind ``Lean.guardMsgsCmd then
403-
return
404-
-- if the linter reached the end of the file, then we scan the `import` syntax instead
405-
let stx := ← do
406-
if stx.isOfKind ``Lean.Parser.Command.eoi then
407-
let fileMap ← getFileMap
408-
-- `impMods` is the syntax for the modules imported in the current file
409-
let (impMods, _) ← Parser.parseHeader
410-
{ input := fileMap.source, fileName := ← getFileName, fileMap := fileMap }
411-
return impMods
412-
else return stx
413-
let sstr := stx.getSubstring?
414-
let fm ← getFileMap
415-
let longLines := ((sstr.getD default).splitOn "\n").filter fun line ↦
416-
(100 < (fm.toPosition line.stopPos).column)
417-
for line in longLines do
418-
if (line.splitOn "http").length ≤ 1 then
419-
let stringMsg := if line.contains '"' then
420-
"\nYou can use \"string gaps\" to format long strings: within a string quotation, \
421-
using a '\\' at the end of a line allows you to continue the string on the following \
422-
line, removing all intervening whitespace."
423-
else ""
424-
Linter.logLint linter.style.longLine (.ofRange ⟨line.startPos, line.stopPos⟩)
425-
m!"This line exceeds the 100 character limit, please shorten it!{stringMsg}"
426-
initialize addLinter longLineLinter
427-
428-
end Style.longLine
429-
430-
end Mathlib.Linter
99+
end Mathlib.Linter.DupNamespaceLinter

0 commit comments

Comments
 (0)