Skip to content
Merged
45 changes: 45 additions & 0 deletions src/Lean/DeprecatedModule.lean
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions src/Lean/Elab/BuiltinCommand.lean
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions src/Lean/Elab/Import.lean
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)

/--
Expand All @@ -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>"
Expand Down
5 changes: 3 additions & 2 deletions src/Lean/Language/Lean.lean
Original file line number Diff line number Diff line change
Expand Up @@ -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⟩) <|
Expand All @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/Lean/Parser/Command.lean
Original file line number Diff line number Diff line change
Expand Up @@ -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
/--
Expand Down
30 changes: 30 additions & 0 deletions tests/elab/deprecatedModule.lean
Original file line number Diff line number Diff line change
@@ -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 changes: 38 additions & 0 deletions tests/elab/deprecated_module.lean
Original file line number Diff line number Diff line change
@@ -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")
4 changes: 4 additions & 0 deletions tests/pkg/deprecated_module/DeprecatedModule/Consumer.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module

import DeprecatedModule.Old
import DeprecatedModule.OldNoMessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module -- deprecated_module: ignore

import DeprecatedModule.Old
import DeprecatedModule.OldNoMessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module

import DeprecatedModule.OldNoMessage
import DeprecatedModule.Old -- deprecated_module: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module

import DeprecatedModule.Old -- deprecated_module: ignore
import DeprecatedModule.OldNoMessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module

import DeprecatedModule.Old -- deprecated_module: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module -- deprecated_module: ignore

import DeprecatedModule.Old
import DeprecatedModule.OldNoMessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module -- deprecated_module: ignore

import DeprecatedModule.Old -- deprecated_module: ignore
import DeprecatedModule.OldNoMessage

#show_deprecated_modules
3 changes: 3 additions & 0 deletions tests/pkg/deprecated_module/DeprecatedModule/New.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module

def newFunction := 42
5 changes: 5 additions & 0 deletions tests/pkg/deprecated_module/DeprecatedModule/Old.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module

import DeprecatedModule.New

deprecated_module "use DeprecatedModule.New instead" (since := "2026-03-19")
6 changes: 6 additions & 0 deletions tests/pkg/deprecated_module/DeprecatedModule/OldDouble.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module

import DeprecatedModule.New

deprecated_module "first deprecation" (since := "2026-03-19")
deprecated_module "second deprecation" (since := "2026-03-20")
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module

deprecated_module (since := "2026-03-19")
3 changes: 3 additions & 0 deletions tests/pkg/deprecated_module/DeprecatedModule/Transitive.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module

import DeprecatedModule.Old
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module

import DeprecatedModule.Transitive
20 changes: 20 additions & 0 deletions tests/pkg/deprecated_module/lakefile.toml
Original file line number Diff line number Diff line change
@@ -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",
]
Loading
Loading