Skip to content

Commit de388a7

Browse files
authored
feat: unknown identifier code action and the module system (#11164)
This PR ensures that the code action provided on unknown identifiers correctly inserts `public` and/or `meta` in `module`s
1 parent 5fff9fb commit de388a7

File tree

9 files changed

+602
-152
lines changed

9 files changed

+602
-152
lines changed

src/Lean/Elab/Command.lean

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,8 @@ private def mkInfoTree (elaborator : Name) (stx : Syntax) (trees : PersistentArr
377377
let scope := s.scopes.head!
378378
let tree := InfoTree.node (Info.ofCommandInfo { elaborator, stx }) trees
379379
let ctx := PartialContextInfo.commandCtx {
380-
env := s.env, fileMap := ctx.fileMap, mctx := {}, currNamespace := scope.currNamespace,
380+
env := s.env, cmdEnv? := some s.env, fileMap := ctx.fileMap, mctx := {},
381+
currNamespace := scope.currNamespace,
381382
openDecls := scope.openDecls, options := scope.opts, ngen := s.ngen
382383
}
383384
return InfoTree.context ctx tree

src/Lean/Elab/InfoTree/Main.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def PartialContextInfo.mergeIntoOuter?
5252
| .autoImplicitCtx _, none =>
5353
panic! "Unexpected incomplete InfoTree context info."
5454
| .commandCtx innerInfo, some outer =>
55-
some { outer with toCommandContextInfo := innerInfo }
55+
some { outer with toCommandContextInfo := { innerInfo with cmdEnv? := outer.cmdEnv? <|> innerInfo.cmdEnv? } }
5656
| .parentDeclCtx innerParentDecl, some outer =>
5757
some { outer with parentDecl? := innerParentDecl }
5858
| .autoImplicitCtx innerAutoImplicits, some outer =>

src/Lean/Elab/InfoTree/Types.lean

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,23 @@ public section
1818
namespace Lean.Elab
1919

2020
/--
21-
Context after executing `liftTermElabM`.
21+
Context at the `CommandElabM/TermElabM` level. Created by `elabCommand` at the top level and then
22+
nestedly when relevant fields are affected (e.g. just before discarding the `mctx` when exiting from
23+
`TermElabM`).
24+
2225
Note that the term information collected during elaboration may contain metavariables, and their
2326
assignments are stored at `mctx`.
2427
-/
2528
structure CommandContextInfo where
2629
env : Environment
30+
/--
31+
Final environment at the end of `elabCommand`; empty for nested contexts. This environment can be
32+
used to access information about the fully-elaborated current declaration such as declaration
33+
ranges. It may not be a strict superset of `env` in case of backtracking, so `env` should be
34+
preferred to access information about the elaboration context at the time this context object was
35+
created.
36+
-/
37+
cmdEnv? : Option Environment := none
2738
fileMap : FileMap
2839
mctx : MetavarContext := {}
2940
options : Options := {}

src/Lean/Elab/MutualDef.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,7 @@ where
12001200
-- encoded name in e.g. kernel errors where it's hard to replace it)
12011201
views.any (fun view => view.kind != .example || view.modifiers.isPublic) &&
12021202
expandedDeclIds.any (!isPrivateName ·.declName)) do
1203+
withSaveInfoContext do -- save adjusted env in info tree
12031204
let headers ← elabHeaders views expandedDeclIds bodyPromises tacPromises
12041205
let headers ← levelMVarToParamHeaders views headers
12051206
if let (#[view], #[declId]) := (views, expandedDeclIds) then
@@ -1324,6 +1325,7 @@ where
13241325
-- Never export private decls from theorem bodies to make sure they stay irrelevant for rebuilds
13251326
withOptions (fun opts =>
13261327
if headers.any (·.kind.isTheorem) then ResolveName.backward.privateInPublic.set opts false else opts) do
1328+
withSaveInfoContext do -- save adjusted env in info tree
13271329
let headers := headers.map fun header =>
13281330
{ header with modifiers.attrs := header.modifiers.attrs.filter (!·.name ∈ [`expose, `no_expose]) }
13291331
let values ← try

src/Lean/Server/CodeActions/UnknownIdentifier.lean

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ structure Insertion where
9292
edit : TextEdit
9393

9494
structure Query extends LeanModuleQuery where
95-
env : Environment
95+
ctx : Elab.ContextInfo
9696
determineInsertion : Name → Insertion
9797

9898
partial def collectOpenNamespaces (currentNamespace : Name) (openDecls : List OpenDecl)
@@ -121,7 +121,7 @@ def computeIdQuery?
121121
return {
122122
identifier := id.toString
123123
openNamespaces := collectOpenNamespaces ctx.currNamespace ctx.openDecls
124-
env := ctx.env
124+
ctx
125125
determineInsertion decl :=
126126
let minimizedId := minimizeGlobalIdentifierInContext ctx.currNamespace ctx.openDecls decl
127127
{
@@ -155,7 +155,7 @@ def computeDotQuery?
155155
return some {
156156
identifier := String.Pos.Raw.extract text.source pos tailPos
157157
openNamespaces := typeNames.map (.allExcept · #[])
158-
env := ctx.env
158+
ctx
159159
determineInsertion decl :=
160160
{
161161
fullName := decl
@@ -186,7 +186,7 @@ def computeDotIdQuery?
186186
return some {
187187
identifier := id.toString
188188
openNamespaces := typeNames.map (.allExcept · #[])
189-
env := ctx.env
189+
ctx
190190
determineInsertion decl :=
191191
{
192192
fullName := decl
@@ -237,6 +237,22 @@ def importAllUnknownIdentifiersCodeAction (params : CodeActionParams) (kind : St
237237
}
238238
}
239239

240+
private def mkImportText (ctx : Elab.ContextInfo) (mod : Name) :
241+
String := Id.run do
242+
let mut text := s!"import {mod}\n"
243+
if let some parentDecl := ctx.parentDecl? then
244+
if isMarkedMeta ctx.env parentDecl then
245+
text := s!"meta {text}"
246+
if !isPrivateName parentDecl then
247+
-- As `meta` declarations go through a second, stricter visibility check in the compiler,
248+
-- we should add `public` anywhere in a public definition (technically even private defs
249+
-- could require public imports but that is not something we can check for here).
250+
text := s!"public {text}"
251+
else if ctx.env.isExporting then
252+
-- Outside `meta`, add `public` only from public scope
253+
text := s!"public {text}"
254+
text
255+
240256
def handleUnknownIdentifierCodeAction
241257
(id : JsonRpc.RequestID)
242258
(params : CodeActionParams)
@@ -276,8 +292,8 @@ def handleUnknownIdentifierCodeAction
276292
| return #[]
277293
for query in queries, result in response.queryResults do
278294
for ⟨mod, decl, isExactMatch⟩ in result do
279-
let isDeclInEnv := query.env.contains decl
280-
if ! isDeclInEnv && mod == query.env.mainModule then
295+
let isDeclInEnv := query.ctx.env.contains decl
296+
if ! isDeclInEnv && mod == query.ctx.env.mainModule then
281297
-- Don't offer any code actions for identifiers defined further down in the same file
282298
continue
283299
let insertion := query.determineInsertion decl
@@ -290,7 +306,7 @@ def handleUnknownIdentifierCodeAction
290306
edits := #[
291307
{
292308
range := importInsertionRange
293-
newText := s!"import {mod}\n"
309+
newText := mkImportText query.ctx mod
294310
},
295311
insertion.edit
296312
]
@@ -344,15 +360,15 @@ def handleResolveImportAllUnknownIdentifiersCodeAction?
344360
let mut imports : Std.HashSet Name := ∅
345361
for q in queries, result in response.queryResults do
346362
let some ⟨mod, decl, _⟩ := result.find? fun id =>
347-
id.isExactMatch && ! q.env.contains id.decl
363+
id.isExactMatch && ! q.ctx.env.contains id.decl
348364
| continue
349-
if mod == q.env.mainModule then
365+
if mod == q.ctx.env.mainModule then
350366
continue
351367
let insertion := q.determineInsertion decl
352368
if ! imports.contains mod then
353369
edits := edits.push {
354370
range := importInsertionRange
355-
newText := s!"import {mod}\n"
371+
newText := mkImportText q.ctx mod
356372
}
357373
edits := edits.push insertion.edit
358374
imports := imports.insert mod

src/Lean/Server/References.lean

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,17 @@ def toLspRefInfo (i : RefInfo) : StateT Decls BaseIO Lsp.RefInfo := do
8686
let refToRefInfoLocation (ref : Reference) : StateT Decls BaseIO RefInfo.Location := do
8787
let parentDeclName? := ref.ci.parentDecl?
8888
let parentDeclNameString? := parentDeclName?.map (·.toString)
89-
let .ok parentDeclInfo? ← EIO.toBaseIO <| ref.ci.runCoreM do
90-
let some parentDeclName := parentDeclName?
91-
| return none
92-
-- Use `local` as it avoids unnecessary blocking, which is especially important when called
93-
-- from the snapshot reporter. Specifically, if `ref` is from a tactic of an async theorem,
94-
-- `parentDeclName` will not be available in the current environment and we would block only
95-
-- to return `none` in the end anyway. At the end of elaboration, we rerun this function on
96-
-- the full info tree with the main environment, so the access will succeed immediately.
97-
let some parentDeclRanges := declRangeExt.find? (asyncMode := .local) (← getEnv) parentDeclName
98-
| return none
99-
return some <| .ofDeclarationRanges parentDeclRanges
100-
-- we only use `CoreM` to get access to a `MonadEnv`, but these are currently all `IO`
101-
| unreachable!
89+
let parentDeclInfo? : Option DeclInfo := do
90+
-- Use final command env that has decl ranges for current declaration as well
91+
let cmdEnv ← ref.ci.cmdEnv?
92+
let parentDeclName ← parentDeclName?
93+
-- Use `local` as it avoids unnecessary blocking, which is especially important when called
94+
-- from the snapshot reporter. Specifically, if `ref` is from a tactic of an async theorem,
95+
-- `parentDeclName` will not be available in the current environment and we would block only
96+
-- to return `none` in the end anyway. At the end of elaboration, we rerun this function on
97+
-- the full info tree with the main environment, so the access will succeed immediately.
98+
let parentDeclRanges ← declRangeExt.find? (asyncMode := .local) cmdEnv parentDeclName
99+
return .ofDeclarationRanges parentDeclRanges
102100
if let some parentDeclNameString := parentDeclNameString? then
103101
if let some parentDeclInfo := parentDeclInfo? then
104102
modify (·.insert parentDeclNameString parentDeclInfo)

src/Lean/Server/Test/Runner.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,9 +692,9 @@ partial def main (args : List String) : IO Unit := do
692692
let isProject := args[0]?.any (· == "-p")
693693
let (ipcCmd, ipcArgs) :=
694694
if isProject then
695-
("lake", #["serve", "--", "-DstderrAsMessages=false"])
695+
("lake", #["serve", "--", "-DstderrAsMessages=false", "-Dexperimental.module=true"])
696696
else
697-
("lean", #["--server", "-DstderrAsMessages=false"])
697+
("lean", #["--server", "-DstderrAsMessages=false", "-Dexperimental.module=true"])
698698
let path := if args.size == 1 then args[0]! else args[1]!
699699
let uri := s!"file:///{path}"
700700
-- We want `dbg_trace` tactics to write directly to stderr instead of being caught in reuse

tests/lean/interactive/unknownIdentifierCodeActions.lean

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
module
12
--^ waitForILeans
3+
4+
public section
5+
26
#check Lean.Server.Test.Refs.test1
37
--^ codeAction
48

@@ -48,3 +52,13 @@ example (f : Foobar) : Nat := f.veryLongAndHopefullyVeryUniqueBar
4852

4953
example : Foobar := .veryLongAndHopefullyVeryUniqueFoobar
5054
--^ codeAction
55+
56+
def pubNonExposed : Lean.Server.Test.Refs.Test1
57+
--^ codeAction
58+
:= Lean.Server.Test.Refs.Test1
59+
--^ codeAction
60+
61+
public meta def pubMeta : Lean.Server.Test.Refs.Test1
62+
--^ codeAction
63+
:= Lean.Server.Test.Refs.Test1
64+
--^ codeAction

0 commit comments

Comments
 (0)