Skip to content

Commit 67526ae

Browse files
author
Wojciech Różowski
authored
feat: introduce findMatchingDecl? for code quality checks in Lean.Linter (#14880)
This PR adds `Lean.Linter.findMatchingDecl?`, which finds the declaration a piece of syntax belongs to by searching the current command's info trees for declarations whose declaration range contains it, together with `Lean.Linter.findCodeQualitySource?` and `Lean.Linter.findCodeQualitySource`, which turn that declaration into a code-quality `Source` (the latter falling back to the current module when no declaration matches). This gives linters a way to attribute diagnostics and code-quality entries to the declaration being elaborated. The helpers are exercised by a new package test, `tests/pkg/find_matching_decl`, which registers a linter reporting the matched declaration and derived source for each command. It covers plain definitions, inductives, mutual blocks, classes, instances, and commands with no associated declaration.
1 parent 5877442 commit 67526ae

6 files changed

Lines changed: 115 additions & 1 deletion

File tree

src/Lean/Linter/Util.lean

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ prelude
99
public import Lean.Server.InfoUtils
1010
public import Lean.Linter.Init
1111
public import Lean.Elab.Term
12+
public import Lean.Linter.CodeQuality.Basic
1213

1314
public section
1415

1516
namespace Lean.Linter
1617

17-
open Lean.Elab
18+
open Lean.Elab CodeQuality
1819

1920
/-- Go upwards through the given `tree` starting from the smallest node that
2021
contains the given `range` and collect all `MacroExpansionInfo`s on the way up.
@@ -74,3 +75,38 @@ def getNewDecls (t : InfoTree) : List Name :=
7475
else
7576
acc
7677
| _ => acc
78+
79+
/-- Find the declaration in the current file that `stx` belongs to.
80+
81+
Searches the info trees of the current command for declarations (see `getNewDecls`) whose
82+
declaration range contains the range of `stx`, and returns the one whose range starts earliest.
83+
Returns `none` if `stx` has no position or no declaration's range contains it (e.g. for commands
84+
such as `#check` or `open`).
85+
-/
86+
def findMatchingDecl? [Monad m] [MonadInfoTree m] [MonadEnv m] [MonadFileMap m]
87+
(stx : Syntax) : m (Option Name) := do
88+
let some stxRange ← getDeclarationRange? stx | return none
89+
let mut best? : Option (Name × DeclarationRange) := none
90+
for t in ← getInfoTrees do
91+
for declName in getNewDecls t do
92+
let some ranges ← findDeclarationRangesCore? declName | continue
93+
let r := ranges.range
94+
unless !r.pos.lt stxRange.pos && !stxRange.endPos.lt r.endPos do continue
95+
if best?.all fun (_, b) => r.pos.lt b.pos then
96+
best? := some (declName, r)
97+
return best?.map (·.1)
98+
99+
/-- Build a code-quality `Source` attributing `stx` to the declaration it belongs to
100+
(see `findMatchingDecl?`), or `none` if no declaration matches. -/
101+
def findCodeQualitySource? [Monad m] [MonadInfoTree m] [MonadEnv m] [MonadFileMap m]
102+
(stx : Syntax) : m (Option Source) := do
103+
let some name ← findMatchingDecl? stx | return none
104+
return some (.declaration (← getEnv).mainModule name)
105+
106+
/-- Build a code-quality `Source` attributing `stx` to the declaration it belongs to
107+
(see `findMatchingDecl?`), falling back to the current module if no declaration matches. -/
108+
def findCodeQualitySource [Monad m] [MonadInfoTree m] [MonadEnv m] [MonadFileMap m]
109+
(stx : Syntax) : m Source := do
110+
match ← findMatchingDecl? stx with
111+
| some name => return .declaration (← getEnv).mainModule name
112+
| none => return .module (← getEnv).mainModule
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import FindMatchingDecl.Linter
2+
3+
/-! Declarations of various shapes for `Linter.findMatchingDecl?` to match: plain definitions,
4+
an inductive, a mutual block whose definitions are compiled via `partial_fixpoint`, a class,
5+
and an anonymous instance. -/
6+
7+
def a := 1
8+
def b := 2
9+
def c := 3
10+
11+
inductive Foo where
12+
| mk : Foo
13+
14+
mutual
15+
def hello2 : Option Nat := hello1
16+
partial_fixpoint
17+
18+
def hello1 : Option Nat := hello2
19+
partial_fixpoint
20+
end
21+
22+
class Magma (carrier : Type u) where
23+
op : carrier → carrier → carrier
24+
25+
instance : Magma Nat where
26+
op := (· + ·)
27+
28+
-- A command with no matching declaration: `findCodeQualitySource` falls back to the module.
29+
#check Nat
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Lean.Elab.Command
2+
import Lean.Linter.Util
3+
4+
/-! A linter that reports the declaration `Linter.findMatchingDecl?` associates with each
5+
command it runs on, and the code-quality source derived from it by
6+
`Linter.findCodeQualitySource` and `Linter.findCodeQualitySource?`. -/
7+
8+
open Lean Elab Command
9+
10+
def matchingDeclLinter : Linter where
11+
run stx := do
12+
if let some decl := (← Linter.findMatchingDecl? stx) then
13+
logInfoAt stx m!"best match is: {decl}"
14+
let src ← Linter.findCodeQualitySource stx
15+
let src? ← Linter.findCodeQualitySource? stx
16+
-- `compress` keeps each message on one line so the test can grep for it
17+
logInfoAt stx m!"source: {(toJson src).compress}; source?: {(toJson src?).compress}"
18+
19+
initialize addLinter matchingDeclLinter
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name = "find_matching_decl"
2+
defaultTargets = ["FindMatchingDecl"]
3+
4+
[[lean_lib]]
5+
name = "FindMatchingDecl"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../build/release/stage1
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
rm -rf .lake
2+
3+
# Build the library; the linter registered in `FindMatchingDecl.Linter` runs on each
4+
# command of `FindMatchingDecl` and reports the declaration `findMatchingDecl?` picks.
5+
capture lake build
6+
7+
check_out_contains "FindMatchingDecl.lean:7:0: best match is: a"
8+
check_out_contains "FindMatchingDecl.lean:8:0: best match is: b"
9+
check_out_contains "FindMatchingDecl.lean:9:0: best match is: c"
10+
check_out_contains "FindMatchingDecl.lean:11:0: best match is: Foo"
11+
# The whole `mutual` command resolves to the earliest declaration in the block.
12+
check_out_contains "FindMatchingDecl.lean:14:0: best match is: hello2"
13+
check_out_contains "FindMatchingDecl.lean:22:0: best match is: Magma"
14+
# An anonymous instance resolves to its generated name.
15+
check_out_contains "FindMatchingDecl.lean:25:0: best match is: instMagmaNat"
16+
17+
# Commands inside a declaration yield a declaration source, in both variants.
18+
check_out_contains 'FindMatchingDecl.lean:7:0: source: {"declaration":{"module":"FindMatchingDecl","name":"a"}}; source?: {"declaration":{"module":"FindMatchingDecl","name":"a"}}'
19+
check_out_contains 'FindMatchingDecl.lean:14:0: source: {"declaration":{"module":"FindMatchingDecl","name":"hello2"}}'
20+
check_out_contains 'FindMatchingDecl.lean:25:0: source: {"declaration":{"module":"FindMatchingDecl","name":"instMagmaNat"}}'
21+
# The module docstring and `#check` match no declaration: the total variant falls back to the
22+
# module source while the optional variant returns `none`.
23+
check_out_contains 'FindMatchingDecl.lean:3:0: source: {"module":{"name":"FindMatchingDecl"}}; source?: null'
24+
check_out_contains 'FindMatchingDecl.lean:30:0: source: {"module":{"name":"FindMatchingDecl"}}; source?: null'

0 commit comments

Comments
 (0)