@@ -4,6 +4,7 @@ Released under Apache 2.0 license as described in the file LICENSE.
44Authors: Anne Baanen
55-/
66import Lean.Elab.Command
7+ import Lean.Elab.ParseImportsFast
78
89/-! # The `directoryDependency` linter
910
@@ -12,6 +13,14 @@ independent. By specifying that one directory does not import from another, we c
1213modularity of Mathlib.
1314-/
1415
16+ -- XXX: is there a better long-time place for this
17+ /-- Parse all imports in a text file at `path` and return just their names:
18+ this is just a thin wrapper around `Lean.parseImports'`.
19+ Omit `Init` (which is part of the prelude). -/
20+ def findImports (path : System.FilePath) : IO (Array Lean.Name) := do
21+ return (← Lean.parseImports' (← IO.FS.readFile path) path.toString).imports
22+ |>.map (fun imp ↦ imp.module) |>.erase `Init
23+
1524/-- Find the longest prefix of `n` such that `f` returns `some` (or return `none` otherwise). -/
1625def Lean.Name.findPrefix {α} (f : Name → Option α) (n : Name) : Option α := do
1726 f n <|> match n with
@@ -26,6 +35,13 @@ def Lean.Name.prefixes (n : Name) : NameSet :=
2635 | str n' _ => n'.prefixes
2736 | num n' _ => n'.prefixes
2837
38+ /-- Return the immediate prefix of `n` (if any). -/
39+ def Lean.Name.prefix? (n : Name) : Option Name :=
40+ match n with
41+ | anonymous => none
42+ | str n' _ => some n'
43+ | num n' _ => some n'
44+
2945/-- Collect all prefixes of names in `ns` into a single `NameSet`. -/
3046def Lean.Name.collectPrefixes (ns : Array Name) : NameSet :=
3147 ns.foldl (fun ns n => ns.union n.prefixes) ∅
@@ -117,11 +133,95 @@ def findAny (r : NamePrefixRel) (n₁ : Name) (ns : Array Name) : Option (Name
117133 pure ()
118134 none
119135
136+ /-- Does `r` contain any entries with key `n`? -/
137+ def containsKey (r : NamePrefixRel) (n : Name) : Bool := NameMap.contains r n
138+
120139/-- Is a prefix of `n₁` related to a prefix of `n₂`? -/
121140def contains (r : NamePrefixRel) (n₁ n₂ : Name) : Bool := (r.find n₁ n₂).isSome
122141
142+ /-- Look up all names `m` which are values of some prefix of `n` under this relation. -/
143+ def getAllLeft (r : NamePrefixRel) (n : Name) : NameSet := Id.run do
144+ let matchingPrefixes := n.prefixes.filter (fun prf ↦ r.containsKey prf)
145+ let mut allRules := NameSet.empty
146+ for prfix in matchingPrefixes do
147+ let some rules := RBMap.find? r prfix | unreachable!
148+ allRules := allRules.append rules
149+ allRules
150+
123151end NamePrefixRel
124152
153+ -- TODO: add/extend tests for this linter, to ensure the allow-list works
154+ -- TODO: move the following three lists to a JSON file, for easier evolution over time!
155+ -- Future: enforce that allowed and forbidden keys are disjoint
156+ -- Future: move further directories to use this allow-list instead of the blocklist
157+
158+ /-- `allowedImportDirs` relates module prefixes, specifying that modules with the first prefix
159+ are only allowed to import modules in the second directory.
160+ For directories which are low in the import hierarchy, this opt-out approach is both more ergonomic
161+ (fewer updates needed) and needs less configuration.
162+
163+ We always allow imports of `Init`, `Lean`, `Std`, `Qq` and
164+ `Mathlib.Init` (as well as their transitive dependencies.)
165+ -/
166+ def allowedImportDirs : NamePrefixRel := .ofArray #[
167+ -- This is used to test the linter.
168+ (`MathlibTest.DirectoryDependencyLinter, `Mathlib.Lean),
169+ -- Mathlib.Tactic has large transitive imports: just allow all of mathlib,
170+ -- as we don't care about the details a lot.
171+ (`MathlibTest.Header, `Mathlib),
172+ (`MathlibTest.Header, `Aesop),
173+ (`MathlibTest.Header, `ImportGraph),
174+ (`MathlibTest.Header, `LeanSearchClient),
175+ (`MathlibTest.Header, `Plausible),
176+ (`MathlibTest.Header, `ProofWidgets),
177+ (`MathlibTest.Header, `Qq),
178+ -- (`MathlibTest.Header, `Mathlib.Tactic),
179+ -- (`MathlibTest.Header, `Mathlib.Deprecated),
180+ (`MathlibTest.Header, `Batteries),
181+ (`MathlibTest.Header, `Lake),
182+
183+ (`Mathlib.Util, `Batteries),
184+ (`Mathlib.Util, `Mathlib.Lean),
185+ (`Mathlib.Util, `Mathlib.Tactic),
186+ -- TODO: reduce this dependency by upstreaming `Data.String.Defs to batteries
187+ (`Mathlib.Util.FormatTable, `Mathlib.Data.String.Defs),
188+
189+ (`Mathlib.Lean, `Batteries.CodeAction),
190+ (`Mathlib.Lean, `Batteries.Tactic.Lint),
191+ -- TODO: decide if this is acceptable or should be split in a more fine-grained way
192+ (`Mathlib.Lean, `Batteries),
193+ (`Mathlib.Lean.Expr, `Mathlib.Util),
194+ (`Mathlib.Lean.Meta.RefinedDiscrTree, `Mathlib.Util),
195+ -- Fine-grained exceptions: TODO decide if these are fine, or should be scoped more broadly.
196+ (`Mathlib.Lean.CoreM, `Mathlib.Tactic.ToExpr),
197+ (`Mathlib.Lean.CoreM, `Mathlib.Util.WhatsNew),
198+ (`Mathlib.Lean.Meta.RefinedDiscrTree, `Mathlib.Tactic.Lemma),
199+ (`Mathlib.Lean.Meta.RefinedDiscrTree, `Mathlib.Tactic.TypeStar),
200+ (`Mathlib.Lean.Meta.RefinedDiscrTree, `Mathlib.Tactic.ToAdditive),
201+ (`Mathlib.Lean.Meta.RefinedDiscrTree, `Mathlib.Tactic), -- split this up further?
202+ (`Mathlib.Lean.Meta.RefinedDiscrTree, `Mathlib.Data), -- split this up further?
203+ (`Mathlib.Lean.Meta.RefinedDiscrTree, `Mathlib.Algebra.Notation),
204+ (`Mathlib.Lean.Meta.RefinedDiscrTree, `Mathlib.Data.Notation),
205+ (`Mathlib.Lean.Meta.RefinedDiscrTree, `Mathlib.Data.Array),
206+
207+ (`Mathlib.Lean.Meta.CongrTheorems, `Mathlib.Data),
208+ (`Mathlib.Lean.Meta.CongrTheorems, `Mathlib.Logic),
209+ (`Mathlib.Lean.Meta.CongrTheorems, `Mathlib.Order.Defs),
210+ (`Mathlib.Lean.Meta.CongrTheorems, `Mathlib.Tactic),
211+
212+ (`Mathlib.Lean.Expr.ExtraRecognizers, `Mathlib.Data),
213+ (`Mathlib.Lean.Expr.ExtraRecognizers, `Mathlib.Order),
214+ (`Mathlib.Lean.Expr.ExtraRecognizers, `Mathlib.Logic),
215+ (`Mathlib.Lean.Expr.ExtraRecognizers, `Mathlib.Tactic),
216+
217+ (`Mathlib.Tactic.Linter, `Batteries),
218+ -- The Mathlib.Tactic.Linter *module* imports all linters, hence requires all the imports.
219+ -- For more fine-grained exceptions of the next two imports, one needs to rename that file.
220+ (`Mathlib.Tactic.Linter, `ImportGraph),
221+ (`Mathlib.Tactic.Linter, `Mathlib.Tactic.MinImports),
222+ (`Mathlib.Tactic.Linter.TextBased, `Mathlib.Data.Nat.Notation),
223+ ]
224+
125225/-- `forbiddenImportDirs` relates module prefixes, specifying that modules with the first prefix
126226should not import modules with the second prefix (except if specifically allowed in
127227`overrideAllowedImportDirs`).
@@ -496,25 +596,81 @@ end DirectoryDependency
496596
497597open DirectoryDependency
498598
499- @ [inherit_doc Mathlib.Linter.linter.directoryDependency]
500- def directoryDependencyCheck (mainModule : Name) : CommandElabM (Option MessageData) := do
501- unless Linter.getLinterValue linter.directoryDependency (← getOptions) do
502- return none
503- let env ← getEnv
504- let imports := env.allImportedModuleNames
599+ /-- Check if one of the imports `imports` to `mainModule` is forbidden by `forbiddenImportDirs`;
600+ if so, return an error describing how the import transitively arises. -/
601+ private def checkBlocklist (env : Environment) (mainModule : Name) (imports : Array Name) : Option MessageData := Id.run do
505602 match forbiddenImportDirs.findAny mainModule imports with
506603 | some (n₁, n₂) => do
507604 if let some imported := n₂.prefixToName imports then
508605 if !overrideAllowedImportDirs.contains mainModule imported then
509- let mut msg := m! "Modules starting with { n₁} are not allowed to import modules starting with { n₂} .
510- This module depends on { imported} \n "
606+ let mut msg := m! "Modules starting with { n₁} are not allowed to import modules starting with { n₂} . \
607+ This module depends on { imported} \n "
511608 for dep in env.importPath imported do
512609 msg := msg ++ m! "which is imported by { dep} ,\n "
513- return some <| msg ++ m! "which is imported by this module.
514- (Exceptions can be added to `overrideAllowedImportDirs`.)"
610+ return some (msg ++ m! "which is imported by this module. \
611+ (Exceptions can be added to `overrideAllowedImportDirs`.)" )
612+ else none
515613 else
516- return some m! "Internal error in `directoryDependency` linter: this module claims to depend on a module starting with { n₂} but a module with that prefix was not found in the import graph."
517- | none => pure ()
518- return none
614+ return some m! "Internal error in `directoryDependency` linter: this module claims to depend \
615+ on a module starting with { n₂} but a module with that prefix was not found in the import graph."
616+ | none => none
617+
618+ @ [inherit_doc Mathlib.Linter.linter.directoryDependency]
619+ def directoryDependencyCheck (mainModule : Name) : CommandElabM (Array MessageData) := do
620+ unless Linter.getLinterValue linter.directoryDependency (← getOptions) do
621+ return #[]
622+ let env ← getEnv
623+ let imports := env.allImportedModuleNames
624+
625+ -- If this module is in the allow-list, we only allow imports from directories specified there.
626+ -- Collect all prefixes which have a matching entry.
627+ let matchingPrefixes := mainModule.prefixes.filter (fun prf ↦ allowedImportDirs.containsKey prf)
628+ if matchingPrefixes.isEmpty then
629+ -- Otherwise, we fall back to the blocklist `forbiddenImportDirs`.
630+ if let some msg := checkBlocklist env mainModule imports then return #[msg] else return #[]
631+ else
632+ -- We always allow imports in the same directory (for each matching prefix),
633+ -- and from `Init`, `Lean`, `Std` and `Qq`.
634+ -- We also allow transitive imports of Mathlib.Init, as well as Mathlib.Init itself.
635+ let initImports := (← findImports ("Mathlib" / "Init.lean" )).append
636+ #[`Mathlib.Init, `Mathlib.Tactic.DeclarationNames]
637+ let exclude := [
638+ `Init, `Std, `Lean, `Qq,
639+ ]
640+ let importsToCheck := imports.filter (fun imp ↦ !exclude.any (·.isPrefixOf imp))
641+ |>.filter (fun imp ↦ !matchingPrefixes.any (·.isPrefixOf imp))
642+ |>.filter (!initImports.contains ·)
643+
644+ -- Find all prefixes which are allowed for one of these directories.
645+ let allRules := allowedImportDirs.getAllLeft mainModule
646+ -- Error about those imports which are not covered by allowedImportDirs.
647+ let mut messages := #[]
648+ for imported in importsToCheck do
649+ if !allowedImportDirs.contains mainModule imported then
650+ let importPath := env.importPath imported
651+ let mut msg := m! "Module { mainModule} depends on { imported} ,\n \
652+ but is only allowed to import modules starting with one of { allRules.toArray.qsort (·.toString < ·.toString)} .\n \
653+ Note: module { imported} "
654+ let mut superseded := false
655+ match importPath.toList with
656+ | [] => msg := msg ++ " is directly imported by this module"
657+ | a :: rest =>
658+ -- Only add messages about imports that aren't themselves transitive imports of
659+ -- forbidden imports.
660+ -- This should prevent redundant messages.
661+ if !allowedImportDirs.contains mainModule a then
662+ superseded := true
663+ else
664+ msg := msg ++ s! " is imported by { a} ,\n "
665+ for dep in rest do
666+ if !allowedImportDirs.contains mainModule dep then
667+ superseded := true
668+ break
669+ msg := msg ++ m! "which is imported by { dep} ,\n "
670+ msg := msg ++ m! "which is imported by this module."
671+ msg := msg ++ "(Exceptions can be added to `allowedImportDirs`.)"
672+ if !superseded then
673+ messages := messages.push msg
674+ return messages
519675
520676end Mathlib.Linter
0 commit comments