Skip to content

Commit dfd3d18

Browse files
authored
test: improve language server test coverage (#10574)
This PR significantly improves the test coverage of the language server, providing at least a single basic test for every request that is used by the client. It also implements infrastructure for testing all of these requests, e.g. the ability to run interactive tests in a project context and refactors the interactive test runner to be more maintainable. Finally, it also fixes a small bug with the recently implemented unknown identifier code actions for auto-implicits (#10442) that was discovered in testing, where the "import all unambiguous unknown identifiers" code action didn't work correctly on auto-implicit identifiers.
1 parent 7d55c03 commit dfd3d18

File tree

147 files changed

+7775
-1137
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

147 files changed

+7775
-1137
lines changed

doc/dev/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ All these tests are included by [src/shell/CMakeLists.txt](https://github.com/le
5959
open Foo in
6060
theorem tst2 (h : a ≤ b) : a + 2 ≤ b + 2 :=
6161
Bla.
62-
--^ textDocument/completion
62+
--^ completion
6363
```
6464
In this example, the test driver [`test_single.sh`](https://github.com/leanprover/lean4/tree/master/tests/lean/interactive/test_single.sh) will simulate an
6565
auto-completion request at `Bla.`. The expected output is stored in

src/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,9 @@ install(DIRECTORY "${CMAKE_BINARY_DIR}/lib/" DESTINATION lib
797797

798798
# symlink source into expected installation location for go-to-definition, if file system allows it
799799
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/src)
800+
# get rid of all files in `src/lean` that may have been loaded from the cache
801+
# (at the time of writing this, this is the case for some lake test .c files)
802+
file(REMOVE_RECURSE ${CMAKE_BINARY_DIR}/src/lean)
800803
if(${STAGE} EQUAL 0)
801804
file(CREATE_LINK ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_BINARY_DIR}/src/lean RESULT _IGNORE_RES SYMBOLIC)
802805
else()

src/Init/Notation.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ applications of this function as `↑` when printing expressions.
643643
syntax (name := Attr.coe) "coe" : attr
644644

645645
/--
646-
This attribute marks a code action, which is used to suggest new tactics or replace existing ones.
646+
This attribute marks a code action that triggers on specific commands.
647647
648648
* `@[command_code_action kind]`: This is a code action which applies to applications of the command
649649
`kind` (a command syntax kind), which can replace the command or insert things before or after it.

src/Lean/Data/Lsp/Ipc.lean

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public import Lean.Data.Lsp.Communication
1313
public import Lean.Data.Lsp.Diagnostics
1414
public import Lean.Data.Lsp.Extra
1515
import Init.Data.List.Sort.Basic
16+
public import Lean.Data.Lsp.LanguageFeatures
1617

1718
public section
1819

@@ -160,10 +161,191 @@ where
160161
| Except.error inner => throw $ userError s!"Cannot decode publishDiagnostics parameters\n{inner}"
161162
| _ => loop
162163

163-
def runWith (lean : System.FilePath) (args : Array String := #[]) (test : IpcM α) : IO α := do
164+
structure CallHierarchy where
165+
item : CallHierarchyItem
166+
fromRanges : Array Range
167+
children : Array CallHierarchy
168+
deriving FromJson, ToJson
169+
170+
partial def expandIncomingCallHierarchy (requestNo : Nat) (uri : DocumentUri) (pos : Lsp.Position) : IpcM (Array CallHierarchy × Nat) := do
171+
writeRequest {
172+
id := requestNo
173+
method := "textDocument/prepareCallHierarchy"
174+
param := {
175+
textDocument := { uri }
176+
position := pos
177+
: CallHierarchyPrepareParams
178+
}
179+
}
180+
let r ← readResponseAs requestNo (Option (Array CallHierarchyItem))
181+
let mut requestNo := requestNo + 1
182+
let roots := r.result.getD #[]
183+
let mut hierarchies := #[]
184+
for root in roots do
185+
let (hierarchy, rootRequestNo) ← go requestNo root #[] {}
186+
requestNo := rootRequestNo
187+
hierarchies := hierarchies.push hierarchy
188+
return (hierarchies, requestNo)
189+
where
190+
go (requestNo : Nat) (item : CallHierarchyItem) (fromRanges : Array Range) (visited : Std.TreeSet String) : IpcM (CallHierarchy × Nat) := do
191+
if visited.contains item.name then
192+
return ({ item, fromRanges := #[], children := #[] }, requestNo)
193+
writeRequest {
194+
id := requestNo
195+
method := "callHierarchy/incomingCalls"
196+
param := {
197+
item
198+
: CallHierarchyIncomingCallsParams
199+
}
200+
}
201+
let r ← readResponseAs requestNo (Option (Array CallHierarchyIncomingCall))
202+
let visited : Std.TreeSet String := visited.insert item.name
203+
let mut requestNo := requestNo + 1
204+
let children := r.result.getD #[]
205+
let mut childHierarchies := #[]
206+
for c in children do
207+
let (childHierarchy, childRequestNo) ← go requestNo c.from c.fromRanges visited
208+
childHierarchies := childHierarchies.push childHierarchy
209+
requestNo := childRequestNo
210+
return ({ item, fromRanges, children := childHierarchies }, requestNo)
211+
212+
partial def expandOutgoingCallHierarchy (requestNo : Nat) (uri : DocumentUri) (pos : Lsp.Position) : IpcM (Array CallHierarchy × Nat) := do
213+
writeRequest {
214+
id := requestNo
215+
method := "textDocument/prepareCallHierarchy"
216+
param := {
217+
textDocument := { uri }
218+
position := pos
219+
: CallHierarchyPrepareParams
220+
}
221+
}
222+
let r ← readResponseAs requestNo (Option (Array CallHierarchyItem))
223+
let mut requestNo := requestNo + 1
224+
let roots := r.result.getD #[]
225+
let mut hierarchies := #[]
226+
for root in roots do
227+
let (hierarchy, rootRequestNo) ← go requestNo root #[] {}
228+
requestNo := rootRequestNo
229+
hierarchies := hierarchies.push hierarchy
230+
return (hierarchies, requestNo)
231+
where
232+
go (requestNo : Nat) (item : CallHierarchyItem) (fromRanges : Array Range) (visited : Std.TreeSet String) : IpcM (CallHierarchy × Nat) := do
233+
if visited.contains item.name then
234+
return ({ item, fromRanges := #[], children := #[] }, requestNo)
235+
writeRequest {
236+
id := requestNo
237+
method := "callHierarchy/outgoingCalls"
238+
param := {
239+
item
240+
: CallHierarchyOutgoingCallsParams
241+
}
242+
}
243+
let r ← readResponseAs requestNo (Option (Array CallHierarchyOutgoingCall))
244+
let visited : Std.TreeSet String := visited.insert item.name
245+
let mut requestNo := requestNo + 1
246+
let children := r.result.getD #[]
247+
let mut childHierarchies := #[]
248+
for c in children do
249+
let (childHierarchy, childRequestNo) ← go requestNo c.to c.fromRanges visited
250+
childHierarchies := childHierarchies.push childHierarchy
251+
requestNo := childRequestNo
252+
return ({ item, fromRanges, children := childHierarchies }, requestNo)
253+
254+
structure ModuleHierarchy where
255+
item : LeanImport
256+
children : Array ModuleHierarchy
257+
deriving FromJson, ToJson
258+
259+
partial def expandModuleHierarchyImports (requestNo : Nat) (uri : DocumentUri) : IpcM (Option ModuleHierarchy × Nat) := do
260+
writeRequest {
261+
id := requestNo
262+
method := "$/lean/prepareModuleHierarchy"
263+
param := {
264+
textDocument := { uri }
265+
: LeanPrepareModuleHierarchyParams
266+
}
267+
}
268+
let r ← readResponseAs requestNo (Option LeanModule)
269+
let mut requestNo := requestNo + 1
270+
let some root := r.result
271+
| return (none, requestNo)
272+
let root := {
273+
module := root
274+
kind := { isAll := false, isPrivate := false, metaKind := .full }
275+
}
276+
let (hierarchy, rootRequestNo) ← go requestNo root {}
277+
requestNo := rootRequestNo
278+
return (hierarchy, requestNo)
279+
where
280+
go (requestNo : Nat) (item : LeanImport) (visited : Std.TreeSet String) : IpcM (ModuleHierarchy × Nat) := do
281+
if visited.contains item.module.name then
282+
return ({ item, children := #[] }, requestNo)
283+
writeRequest {
284+
id := requestNo
285+
method := "$/lean/moduleHierarchy/imports"
286+
param := {
287+
module := item.module
288+
: LeanModuleHierarchyImportsParams
289+
}
290+
}
291+
let r ← readResponseAs requestNo (Array LeanImport)
292+
let visited : Std.TreeSet String := visited.insert item.module.name
293+
let mut requestNo := requestNo + 1
294+
let children := r.result
295+
let mut childHierarchies := #[]
296+
for c in children do
297+
let (childHierarchy, childRequestNo) ← go requestNo c visited
298+
childHierarchies := childHierarchies.push childHierarchy
299+
requestNo := childRequestNo
300+
return ({ item, children := childHierarchies }, requestNo)
301+
302+
partial def expandModuleHierarchyImportedBy (requestNo : Nat) (uri : DocumentUri) : IpcM (Option ModuleHierarchy × Nat) := do
303+
writeRequest {
304+
id := requestNo
305+
method := "$/lean/prepareModuleHierarchy"
306+
param := {
307+
textDocument := { uri }
308+
: LeanPrepareModuleHierarchyParams
309+
}
310+
}
311+
let r ← readResponseAs requestNo (Option LeanModule)
312+
let mut requestNo := requestNo + 1
313+
let some root := r.result
314+
| return (none, requestNo)
315+
let root := {
316+
module := root
317+
kind := { isAll := false, isPrivate := false, metaKind := .full }
318+
}
319+
let (hierarchy, rootRequestNo) ← go requestNo root {}
320+
requestNo := rootRequestNo
321+
return (hierarchy, requestNo)
322+
where
323+
go (requestNo : Nat) (item : LeanImport) (visited : Std.TreeSet String) : IpcM (ModuleHierarchy × Nat) := do
324+
if visited.contains item.module.name then
325+
return ({ item, children := #[] }, requestNo)
326+
writeRequest {
327+
id := requestNo
328+
method := "$/lean/moduleHierarchy/importedBy"
329+
param := {
330+
module := item.module
331+
: LeanModuleHierarchyImportedByParams
332+
}
333+
}
334+
let r ← readResponseAs requestNo (Array LeanImport)
335+
let visited : Std.TreeSet String := visited.insert item.module.name
336+
let mut requestNo := requestNo + 1
337+
let children := r.result
338+
let mut childHierarchies := #[]
339+
for c in children do
340+
let (childHierarchy, childRequestNo) ← go requestNo c visited
341+
childHierarchies := childHierarchies.push childHierarchy
342+
requestNo := childRequestNo
343+
return ({ item, children := childHierarchies }, requestNo)
344+
345+
def runWith (lean : String) (args : Array String := #[]) (test : IpcM α) : IO α := do
164346
let proc ← Process.spawn {
165347
toStdioConfig := ipcStdioConfig
166-
cmd := lean.toString
348+
cmd := lean
167349
args := args }
168350
ReaderT.run test proc
169351

src/Lean/Elab/GuardMsgs.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def guardMsgsCodeAction : CommandCodeAction := fun _ _ _ node => do
235235
let some (stx, res) := res | return #[]
236236
let doc ← readDoc
237237
let eager := {
238-
title := "Update #guard_msgs with tactic output"
238+
title := "Update #guard_msgs with generated message"
239239
kind? := "quickfix"
240240
isPreferred? := true
241241
}

src/Lean/Server/CodeActions/Attr.lean

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ public section
1717
* Attribute `@[hole_code_action]` collects code actions which will be called
1818
on each occurrence of a hole (`_`, `?_` or `sorry`).
1919
20-
* Attribute `@[tactic_code_action]` collects code actions which will be called
21-
on each occurrence of a tactic.
22-
2320
* Attribute `@[command_code_action]` collects code actions which will be called
2421
on each occurrence of a command.
2522
-/

src/Lean/Server/CodeActions/UnknownIdentifier.lean

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ namespace Lean.Server.FileWorker
2020
open Lean.Lsp
2121
open Lean.Server.Completion
2222

23+
private def compareRanges (r1 r2 : String.Range) : Ordering :=
24+
if r1.start < r2.start then
25+
.lt
26+
else if r1.start > r2.start then
27+
.gt
28+
else if r1.stop < r2.stop then
29+
.lt
30+
else if r1.stop > r2.stop then
31+
.gt
32+
else
33+
.eq
34+
2335
def waitUnknownIdentifierRanges (doc : EditableDocument) (requestedRange : String.Range)
2436
: BaseIO (Array String.Range × Bool) := do
2537
let text := doc.meta.text
@@ -37,32 +49,46 @@ def waitUnknownIdentifierRanges (doc : EditableDocument) (requestedRange : Strin
3749
continue
3850
ranges := ranges.push msgRange
3951
let isAnyUnknownIdentifierMessage := ! ranges.isEmpty
40-
let autoImplicitUsages := tree.foldInfosInRange requestedRange #[] fun ctx i acc => Id.run do
41-
let .ofTermInfo ti := i
42-
| return acc
43-
let some r := ti.stx.getRange? (canonicalOnly := true)
44-
| return acc
45-
if ! ti.expr.isFVar then
46-
return acc
47-
if ! ctx.autoImplicits.contains ti.expr then
48-
return acc
49-
return acc.push r
50-
let autoImplicitUsages := autoImplicitUsages.get
52+
let autoImplicitUsages : ServerTask (Std.TreeSet String.Range compareRanges) :=
53+
tree.foldInfosInRange requestedRange ∅ fun ctx i acc => Id.run do
54+
let .ofTermInfo ti := i
55+
| return acc
56+
let some r := ti.stx.getRange? (canonicalOnly := true)
57+
| return acc
58+
if ! ti.expr.isFVar then
59+
return acc
60+
if ! ctx.autoImplicits.contains ti.expr then
61+
return acc
62+
return acc.insert r
63+
let autoImplicitUsages := autoImplicitUsages.get.toArray
5164
ranges := ranges ++ autoImplicitUsages
5265
return (ranges, isAnyUnknownIdentifierMessage)
5366

5467
def waitAllUnknownIdentifierMessageRanges (doc : EditableDocument)
5568
: BaseIO (Array String.Range) := do
5669
let text := doc.meta.text
57-
let msgLog : MessageLog := Language.toSnapshotTree doc.initSnap
58-
|>.getAll.map (·.diagnostics.msgLog)
59-
|>.foldl (· ++ ·) {}
70+
let snaps := Language.toSnapshotTree doc.initSnap |>.getAll
71+
let msgLog : MessageLog := snaps.map (·.diagnostics.msgLog) |>.foldl (· ++ ·) {}
6072
let mut ranges := #[]
6173
for msg in msgLog.unreported do
6274
if ! msg.data.hasTag (· == unknownIdentifierMessageTag) then
6375
continue
6476
let msgRange : String.Range := ⟨text.ofPosition msg.pos, text.ofPosition <| msg.endPos.getD msg.pos⟩
6577
ranges := ranges.push msgRange
78+
let (cmdSnaps, _) := doc.cmdSnaps.waitAll.get
79+
for snap in cmdSnaps do
80+
let autoImplicitUsages : Std.TreeSet String.Range compareRanges :=
81+
snap.infoTree.foldInfo (init := ∅) fun ctx i acc => Id.run do
82+
let .ofTermInfo ti := i
83+
| return acc
84+
let some r := ti.stx.getRange? (canonicalOnly := true)
85+
| return acc
86+
if ! ti.expr.isFVar then
87+
return acc
88+
if ! ctx.autoImplicits.contains ti.expr then
89+
return acc
90+
return acc.insert r
91+
ranges := ranges ++ autoImplicitUsages.toArray
6692
return ranges
6793

6894
structure Insertion where

src/Lean/Server/Test.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ module
99
prelude
1010
public import Lean.Server.Test.Cancel
1111
public import Lean.Server.Test.Runner
12+
public import Lean.Server.Test.Refs
1213

1314
public section

src/Lean/Server/Test/Refs.lean

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/-
2+
Copyright (c) 2025 Lean FRO. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
5+
Authors: Marc Huisinga
6+
-/
7+
module
8+
9+
prelude
10+
import Init.Prelude
11+
12+
public def LeanServerTestRefsTest0 := Nat
13+
14+
namespace Lean.Server.Test.Refs
15+
16+
public def Test1 := Nat
17+
public def Test2 := Test1
18+
public def Test3 := Test1
19+
public def Test4 := Test2
20+
public def Test5 := Test2
21+
22+
public inductive Test6
23+
| mk
24+
public def test7 : Test6 := .mk
25+
public def test8 : Test6 := .mk
26+
public def test9 := test7
27+
public def test10 := test9

0 commit comments

Comments
 (0)