@@ -9,7 +9,8 @@ import Mathlib.Tactic.DeclarationNames
9
9
/-!
10
10
# Linters for Mathlib
11
11
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.
13
14
14
15
Perhaps these should be moved to Batteries in the future.
15
16
-/
@@ -95,336 +96,4 @@ def dupNamespace : Linter where run := withSetOptionIn fun stx ↦ do
95
96
96
97
initialize addLinter dupNamespace
97
98
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\n end{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
- "\n You 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