feat: add deprecated_module (#13002)
This PR adds a `deprecated_module` command that marks the current module as deprecated. When another module imports a deprecated module, a warning is emitted during elaboration suggesting replacement imports. Example usage: ```lean deprecated_module "use NewModule instead" (since := "2026-03-30") ``` The warning message is optional but recommended. The `since` parameter is required. Warnings can be disabled, by setting `linter.deprecated.module` option to false in the command line. Because the check happens when importing , using `set_option linter.deprecated.module` in the source file won't affect the warnings. Instead, a whole file can be marked not to display depreciation warnings, by putting a comment `deprecated_module: ignore` next to `module` keyword. Similarly, individual keywords can be silenced. A `#show_deprecated_modules` command is also provided for inspecting which modules in the current environment are deprecated. `linter.deprecated.module` has no effect on this command, and hence one can view deprecated modules, even when having warnings silenced.
This commit is contained in:
parent
8b52f4e8f7
commit
eac9315962
22 changed files with 343 additions and 2 deletions
45
src/Lean/DeprecatedModule.lean
Normal file
45
src/Lean/DeprecatedModule.lean
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/-
|
||||
Copyright (c) 2026 Lean FRO, LLC. All rights reserved.
|
||||
Released under Apache 2.0 license as described in the file LICENSE.
|
||||
Authors: Wojciech Różowski
|
||||
-/
|
||||
module
|
||||
|
||||
prelude
|
||||
public import Lean.Compiler.ModPkgExt
|
||||
|
||||
public section
|
||||
|
||||
namespace Lean
|
||||
|
||||
structure DeprecatedModuleEntry where
|
||||
message? : Option String := none
|
||||
since? : Option String := none
|
||||
deriving Inhabited
|
||||
|
||||
register_builtin_option linter.deprecated.module : Bool := {
|
||||
defValue := true
|
||||
descr := "if true, generate warnings when importing deprecated modules"
|
||||
}
|
||||
|
||||
builtin_initialize deprecatedModuleExt : ModuleEnvExtension <| Option DeprecatedModuleEntry ←
|
||||
registerModuleEnvExtension <| pure none
|
||||
|
||||
def Environment.getDeprecatedModuleByIdx? (env : Environment) (idx : ModuleIdx) : Option DeprecatedModuleEntry :=
|
||||
deprecatedModuleExt.getStateByIdx? env idx |>.join
|
||||
|
||||
def Environment.setDeprecatedModule (entry : Option DeprecatedModuleEntry) (env : Environment) : Environment :=
|
||||
deprecatedModuleExt.setState env entry
|
||||
|
||||
def formatDeprecatedModuleWarning (env : Environment) (idx : ModuleIdx) (modName : Name)
|
||||
(entry : DeprecatedModuleEntry) : String :=
|
||||
let msg := entry.message?.getD ""
|
||||
let replacements := env.header.moduleData[idx.toNat]!.imports.filter fun imp =>
|
||||
imp.module != `Init
|
||||
let lines := replacements.foldl (init := "") fun acc imp =>
|
||||
acc ++ s!"import {imp.module}\n"
|
||||
s!"{msg}\n\
|
||||
'{modName}' has been deprecated: please replace this import by\n\n\
|
||||
{lines}"
|
||||
|
||||
end Lean
|
||||
|
|
@ -14,6 +14,7 @@ public import Lean.Elab.Open
|
|||
import Init.Data.Nat.Order
|
||||
import Init.Data.Order.Lemmas
|
||||
import Init.System.Platform
|
||||
import Lean.DeprecatedModule
|
||||
|
||||
public section
|
||||
|
||||
|
|
@ -716,6 +717,45 @@ where
|
|||
let env ← getEnv
|
||||
IO.eprintln (← env.dbgFormatAsyncState)
|
||||
|
||||
/-- Elaborate `deprecated_module`, marking the current module as deprecated. -/
|
||||
@[builtin_command_elab Parser.Command.deprecated_module]
|
||||
def elabDeprecatedModule : CommandElab
|
||||
| `(Parser.Command.deprecated_module| deprecated_module $[$msg?]? $[(since := $since?)]?) => do
|
||||
let message? := msg?.map TSyntax.getString
|
||||
let since? := since?.map TSyntax.getString
|
||||
if (deprecatedModuleExt.getState (← getEnv)).isSome then
|
||||
logWarning "module is already marked as deprecated"
|
||||
if since?.isNone then
|
||||
logWarning "`deprecated_module` should specify the date or library version \
|
||||
at which the deprecation was introduced, using `(since := \"...\")`"
|
||||
modifyEnv fun env => env.setDeprecatedModule (some { message?, since? })
|
||||
| _ => throwUnsupportedSyntax
|
||||
|
||||
/-- Elaborate `#show_deprecated_modules`, displaying all deprecated modules. -/
|
||||
@[builtin_command_elab Parser.Command.showDeprecatedModules]
|
||||
def elabShowDeprecatedModules : CommandElab := fun _ => do
|
||||
let env ← getEnv
|
||||
let mut parts : Array String := #["Deprecated modules\n"]
|
||||
for h : idx in [:env.header.moduleNames.size] do
|
||||
if let some entry := env.getDeprecatedModuleByIdx? idx then
|
||||
let modName := env.header.moduleNames[idx]
|
||||
let msg := match entry.message? with
|
||||
| some str => s!"message '{str}'"
|
||||
| none => "no message"
|
||||
let replacements := env.header.moduleData[idx]!.imports.filter fun imp =>
|
||||
imp.module != `Init
|
||||
parts := parts.push s!"'{modName}' deprecates to\n{replacements.map (·.module)}\nwith {msg}\n"
|
||||
-- Also show the current module's deprecation if set.
|
||||
if let some entry := deprecatedModuleExt.getState env then
|
||||
let modName := env.mainModule
|
||||
let msg := match entry.message? with
|
||||
| some str => s!"message '{str}'"
|
||||
| none => "no message"
|
||||
let replacements := env.imports.filter fun imp =>
|
||||
imp.module != `Init
|
||||
parts := parts.push s!"'{modName}' deprecates to\n{replacements.map (·.module)}\nwith {msg}\n"
|
||||
logInfo (String.intercalate "\n" parts.toList)
|
||||
|
||||
@[builtin_command_elab Parser.Command.deprecatedSyntax] def elabDeprecatedSyntax : CommandElab := fun stx => do
|
||||
let id := stx[1]
|
||||
let kind ← liftCoreM <| checkSyntaxNodeKindAtNamespaces id.getId (← getCurrNamespace)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ prelude
|
|||
public import Lean.Parser.Module
|
||||
meta import Lean.Parser.Module
|
||||
import Lean.Compiler.ModPkgExt
|
||||
public import Lean.DeprecatedModule
|
||||
|
||||
public section
|
||||
|
||||
|
|
@ -42,12 +43,66 @@ def HeaderSyntax.toModuleHeader (stx : HeaderSyntax) : ModuleHeader where
|
|||
|
||||
abbrev headerToImports := @HeaderSyntax.imports
|
||||
|
||||
/--
|
||||
Check imported modules for deprecation and emit warnings.
|
||||
|
||||
The `-- deprecated_module: ignore` comment can be placed on the `module` keyword to suppress
|
||||
all warnings, or on individual `import` statements to suppress specific ones.
|
||||
This follows the same pattern as `-- shake: keep` in Lake shake.
|
||||
|
||||
The `headerStx?` parameter carries the header syntax used for checking trailing comments.
|
||||
When called from the Language Server, the main header syntax may have its trailing trivia
|
||||
stripped by `unsetTrailing` for caching purposes, so `origHeaderStx?` can supply the original
|
||||
(untrimmed) syntax to preserve `-- deprecated_module: ignore` annotations on the last import.
|
||||
-/
|
||||
def checkDeprecatedImports
|
||||
(env : Environment) (imports : Array Import) (opts : Options)
|
||||
(inputCtx : Parser.InputContext) (startPos : String.Pos.Raw) (messages : MessageLog)
|
||||
(headerStx? : Option HeaderSyntax := none)
|
||||
(origHeaderStx? : Option HeaderSyntax := none)
|
||||
: MessageLog := Id.run do
|
||||
let mut opts := opts
|
||||
let mut ignoreDeprecatedImports : NameSet := {}
|
||||
if let some headerStx := origHeaderStx? <|> headerStx? then
|
||||
match headerStx with
|
||||
| `(Parser.Module.header| $[module%$moduleTk]? $[prelude%$_]? $importsStx*) =>
|
||||
if moduleTk.any (·.getTrailing?.any (·.toString.contains "deprecated_module: ignore")) then
|
||||
opts := linter.deprecated.module.set opts false
|
||||
for impStx in importsStx do
|
||||
if impStx.raw.getTrailing?.any (·.toString.contains "deprecated_module: ignore") then
|
||||
match impStx with
|
||||
| `(Parser.Module.import| $[public%$_]? $[meta%$_]? import $[all%$_]? $n) =>
|
||||
ignoreDeprecatedImports := ignoreDeprecatedImports.insert n.getId
|
||||
| _ => pure ()
|
||||
| _ => pure ()
|
||||
if !linter.deprecated.module.get opts then
|
||||
return messages
|
||||
imports.foldl (init := messages) fun messages imp =>
|
||||
if ignoreDeprecatedImports.contains imp.module then
|
||||
messages
|
||||
else
|
||||
match env.getModuleIdx? imp.module with
|
||||
| some idx =>
|
||||
match env.getDeprecatedModuleByIdx? idx with
|
||||
| some entry =>
|
||||
let pos := inputCtx.fileMap.toPosition startPos
|
||||
messages.add {
|
||||
fileName := inputCtx.fileName
|
||||
pos := pos
|
||||
severity := .warning
|
||||
data := .tagged ``deprecatedModuleExt <| formatDeprecatedModuleWarning env idx imp.module entry
|
||||
}
|
||||
| none => messages
|
||||
| none => messages
|
||||
|
||||
def processHeaderCore
|
||||
(startPos : String.Pos.Raw) (imports : Array Import) (isModule : Bool)
|
||||
(opts : Options) (messages : MessageLog) (inputCtx : Parser.InputContext)
|
||||
(trustLevel : UInt32 := 0) (plugins : Array System.FilePath := #[]) (leakEnv := false)
|
||||
(mainModule := Name.anonymous) (package? : Option PkgId := none)
|
||||
(arts : NameMap ImportArtifacts := {})
|
||||
(headerStx? : Option HeaderSyntax := none)
|
||||
(origHeaderStx? : Option HeaderSyntax := none)
|
||||
: IO (Environment × MessageLog) := do
|
||||
let level := if isModule then
|
||||
if Elab.inServer.get opts then
|
||||
|
|
@ -66,6 +121,7 @@ def processHeaderCore
|
|||
let pos := inputCtx.fileMap.toPosition startPos
|
||||
pure (env, messages.add { fileName := inputCtx.fileName, data := toString e, pos := pos })
|
||||
let env := env.setMainModule mainModule |>.setModulePackage package?
|
||||
let messages := checkDeprecatedImports env imports opts inputCtx startPos messages headerStx? origHeaderStx?
|
||||
return (env, messages)
|
||||
|
||||
/--
|
||||
|
|
@ -82,6 +138,7 @@ backwards compatibility measure not compatible with the module system.
|
|||
: IO (Environment × MessageLog) := do
|
||||
processHeaderCore header.startPos header.imports header.isModule
|
||||
opts messages inputCtx trustLevel plugins leakEnv mainModule
|
||||
(headerStx? := header)
|
||||
|
||||
def parseImports (input : String) (fileName : Option String := none) : IO (Array Import × Position × MessageLog) := do
|
||||
let fileName := fileName.getD "<input>"
|
||||
|
|
|
|||
|
|
@ -478,11 +478,11 @@ where
|
|||
}
|
||||
result? := some {
|
||||
parserState
|
||||
processedSnap := (← processHeader ⟨trimmedStx⟩ parserState)
|
||||
processedSnap := (← processHeader ⟨trimmedStx⟩ stx parserState)
|
||||
}
|
||||
}
|
||||
|
||||
processHeader (stx : HeaderSyntax) (parserState : Parser.ModuleParserState) :
|
||||
processHeader (stx : HeaderSyntax) (origStx : HeaderSyntax) (parserState : Parser.ModuleParserState) :
|
||||
LeanProcessingM (SnapshotTask HeaderProcessedSnapshot) := do
|
||||
let ctx ← read
|
||||
SnapshotTask.ofIO none none (.some ⟨0, ctx.endPos⟩) <|
|
||||
|
|
@ -498,6 +498,7 @@ where
|
|||
let (headerEnv, msgLog) ← Elab.processHeaderCore (leakEnv := true)
|
||||
stx.startPos setup.imports setup.isModule setup.opts .empty ctx.toInputContext
|
||||
setup.trustLevel setup.plugins setup.mainModuleName setup.package? setup.importArts
|
||||
(headerStx? := stx) (origHeaderStx? := origStx)
|
||||
let stopTime := (← IO.monoNanosNow).toFloat / 1000000000
|
||||
let diagnostics := (← Snapshot.Diagnostics.ofMessageLog msgLog)
|
||||
if msgLog.hasErrors then
|
||||
|
|
|
|||
|
|
@ -638,6 +638,27 @@ An internal bootstrapping command that reinterprets a Markdown docstring as Vers
|
|||
-/
|
||||
@[builtin_command_parser] def «docs_to_verso» := leading_parser
|
||||
"docs_to_verso " >> sepBy1 ident ", "
|
||||
/--
|
||||
`deprecated_module` marks the current module as deprecated.
|
||||
When another module imports a deprecated module, a warning is emitted during elaboration.
|
||||
|
||||
```
|
||||
deprecated_module "use NewModule instead" (since := "2026-03-19")
|
||||
```
|
||||
|
||||
The warning message is optional but recommended.
|
||||
The warning can be disabled with `set_option linter.deprecated.module false` or
|
||||
`-Dlinter.deprecated.module=false`.
|
||||
-/
|
||||
@[builtin_command_parser] def «deprecated_module» := leading_parser
|
||||
"deprecated_module" >> optional (ppSpace >> strLit) >> optional (" (" >> nonReservedSymbol "since" >> " := " >> strLit >> ")")
|
||||
|
||||
/--
|
||||
`#show_deprecated_modules` displays all modules in the current environment that have been
|
||||
marked with `deprecated_module`.
|
||||
-/
|
||||
@[builtin_command_parser] def showDeprecatedModules := leading_parser
|
||||
"#show_deprecated_modules"
|
||||
|
||||
def optionValue := nonReservedSymbol "true" <|> nonReservedSymbol "false" <|> strLit <|> numLit
|
||||
/--
|
||||
|
|
|
|||
30
tests/elab/deprecatedModule.lean
Normal file
30
tests/elab/deprecatedModule.lean
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
module
|
||||
|
||||
import Std.Data
|
||||
|
||||
|
||||
deprecated_module (since := "30-03-2026")
|
||||
|
||||
/--
|
||||
info: Deprecated modules
|
||||
|
||||
'elab.deprecatedModule' deprecates to
|
||||
#[Std.Data]
|
||||
with no message
|
||||
-/
|
||||
#guard_msgs in
|
||||
#show_deprecated_modules
|
||||
|
||||
/-- warning: module is already marked as deprecated -/
|
||||
#guard_msgs in
|
||||
deprecated_module "custom message" (since := "30-03-2026")
|
||||
|
||||
/--
|
||||
info: Deprecated modules
|
||||
|
||||
'elab.deprecatedModule' deprecates to
|
||||
#[Std.Data]
|
||||
with message 'custom message'
|
||||
-/
|
||||
#guard_msgs in
|
||||
#show_deprecated_modules
|
||||
38
tests/elab/deprecated_module.lean
Normal file
38
tests/elab/deprecated_module.lean
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/-
|
||||
Tests for the `deprecated_module` command.
|
||||
-/
|
||||
|
||||
-- Missing since (message is optional)
|
||||
/--
|
||||
warning: `deprecated_module` should specify the date or library version at which the deprecation was introduced, using `(since := "...")`
|
||||
-/
|
||||
#guard_msgs in
|
||||
deprecated_module
|
||||
|
||||
-- Missing since with message (also warns about duplicate since module is already marked above)
|
||||
/--
|
||||
warning: module is already marked as deprecated
|
||||
---
|
||||
warning: `deprecated_module` should specify the date or library version at which the deprecation was introduced, using `(since := "...")`
|
||||
-/
|
||||
#guard_msgs in
|
||||
deprecated_module "use NewModule instead"
|
||||
|
||||
-- No message, with since (also warns about duplicate)
|
||||
/-- warning: module is already marked as deprecated -/
|
||||
#guard_msgs in
|
||||
deprecated_module (since := "2026-03-19")
|
||||
|
||||
-- Both message and since: only duplicate warning
|
||||
/--
|
||||
warning: module is already marked as deprecated
|
||||
-/
|
||||
#guard_msgs in
|
||||
deprecated_module "use NewModule instead" (since := "2026-03-19")
|
||||
|
||||
-- Duplicate deprecated_module: warns about already being marked (standalone confirmation)
|
||||
/--
|
||||
warning: module is already marked as deprecated
|
||||
-/
|
||||
#guard_msgs in
|
||||
deprecated_module "use SomethingElse instead" (since := "2026-03-20")
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
module
|
||||
|
||||
import DeprecatedModule.Old
|
||||
import DeprecatedModule.OldNoMessage
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
module -- deprecated_module: ignore
|
||||
|
||||
import DeprecatedModule.Old
|
||||
import DeprecatedModule.OldNoMessage
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
module
|
||||
|
||||
import DeprecatedModule.OldNoMessage
|
||||
import DeprecatedModule.Old -- deprecated_module: ignore
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
module
|
||||
|
||||
import DeprecatedModule.Old -- deprecated_module: ignore
|
||||
import DeprecatedModule.OldNoMessage
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module
|
||||
|
||||
import DeprecatedModule.Old -- deprecated_module: ignore
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
module -- deprecated_module: ignore
|
||||
|
||||
import DeprecatedModule.Old
|
||||
import DeprecatedModule.OldNoMessage
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
module -- deprecated_module: ignore
|
||||
|
||||
import DeprecatedModule.Old -- deprecated_module: ignore
|
||||
import DeprecatedModule.OldNoMessage
|
||||
|
||||
#show_deprecated_modules
|
||||
3
tests/pkg/deprecated_module/DeprecatedModule/New.lean
Normal file
3
tests/pkg/deprecated_module/DeprecatedModule/New.lean
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module
|
||||
|
||||
def newFunction := 42
|
||||
5
tests/pkg/deprecated_module/DeprecatedModule/Old.lean
Normal file
5
tests/pkg/deprecated_module/DeprecatedModule/Old.lean
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module
|
||||
|
||||
import DeprecatedModule.New
|
||||
|
||||
deprecated_module "use DeprecatedModule.New instead" (since := "2026-03-19")
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
module
|
||||
|
||||
import DeprecatedModule.New
|
||||
|
||||
deprecated_module "first deprecation" (since := "2026-03-19")
|
||||
deprecated_module "second deprecation" (since := "2026-03-20")
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module
|
||||
|
||||
deprecated_module (since := "2026-03-19")
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module
|
||||
|
||||
import DeprecatedModule.Old
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module
|
||||
|
||||
import DeprecatedModule.Transitive
|
||||
20
tests/pkg/deprecated_module/lakefile.toml
Normal file
20
tests/pkg/deprecated_module/lakefile.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
name = "deprecated_module"
|
||||
defaultTargets = ["Main"]
|
||||
|
||||
[[lean_lib]]
|
||||
name = "Main"
|
||||
roots = [
|
||||
"DeprecatedModule.New",
|
||||
"DeprecatedModule.Old",
|
||||
"DeprecatedModule.OldNoMessage",
|
||||
"DeprecatedModule.OldDouble",
|
||||
"DeprecatedModule.Transitive",
|
||||
"DeprecatedModule.Consumer",
|
||||
"DeprecatedModule.TransitiveConsumer",
|
||||
"DeprecatedModule.ConsumerIgnoreAll",
|
||||
"DeprecatedModule.ConsumerIgnoreOne",
|
||||
"DeprecatedModule.ConsumerIgnoreWhitespace",
|
||||
"DeprecatedModule.ConsumerIgnoreOnlyImport",
|
||||
"DeprecatedModule.ConsumerIgnoreLastImport",
|
||||
"DeprecatedModule.ConsumerShowDeprecated",
|
||||
]
|
||||
37
tests/pkg/deprecated_module/run_test.sh
Normal file
37
tests/pkg/deprecated_module/run_test.sh
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
rm -rf .lake/build
|
||||
|
||||
# Build Main library — includes all test modules
|
||||
capture lake build Main
|
||||
|
||||
# With-message format: custom message on its own line, then deprecation with replacement imports
|
||||
check_out_contains "use DeprecatedModule.New instead"
|
||||
check_out_contains "'DeprecatedModule.Old' has been deprecated: please replace this import by"
|
||||
check_out_contains "import DeprecatedModule.New"
|
||||
|
||||
# Without-message format: deprecation with replacement imports but no custom message
|
||||
check_out_contains "'DeprecatedModule.OldNoMessage' has been deprecated: please replace this import by"
|
||||
|
||||
# OldDouble has two deprecated_module commands — second triggers duplicate warning
|
||||
check_out_contains "module is already marked as deprecated"
|
||||
|
||||
# TransitiveConsumer only imports Transitive (which imports Old) — no direct import, no warning
|
||||
# (covered implicitly: if transitive warnings leaked, we'd see extra output)
|
||||
|
||||
# ConsumerIgnoreOne: "deprecated_module: ignore" on Old import only — OldNoMessage should still warn
|
||||
check_out_contains "ConsumerIgnoreOne.lean:1:0: 'DeprecatedModule.OldNoMessage' has been deprecated"
|
||||
|
||||
# ConsumerIgnoreOnlyImport: single import with "deprecated_module: ignore" — no warning
|
||||
if grep -Fq "ConsumerIgnoreOnlyImport.lean" "$CAPTURED.out.produced"; then
|
||||
fail "ConsumerIgnoreOnlyImport should not produce any deprecation warning"
|
||||
fi
|
||||
|
||||
# ConsumerIgnoreLastImport: "deprecated_module: ignore" on last import (Old) — OldNoMessage should
|
||||
# still warn, but Old should be suppressed
|
||||
check_out_contains "ConsumerIgnoreLastImport.lean:1:0: 'DeprecatedModule.OldNoMessage' has been deprecated"
|
||||
if grep -Fq "ConsumerIgnoreLastImport.lean:1:0: 'DeprecatedModule.Old' has been deprecated" "$CAPTURED.out.produced"; then
|
||||
fail "ConsumerIgnoreLastImport should not warn about Old (annotated with deprecated_module: ignore)"
|
||||
fi
|
||||
|
||||
# ConsumerShowDeprecated: #show_deprecated_modules should still list deprecated modules
|
||||
# even when warnings are suppressed via "deprecated_module: ignore"
|
||||
check_out_contains "ConsumerShowDeprecated.lean:6:0: Deprecated modules"
|
||||
Loading…
Add table
Reference in a new issue