From 3411f63ad7162b8b399abe481ec49f4dbc75949e Mon Sep 17 00:00:00 2001 From: Joris Blaak Date: Sat, 25 Apr 2026 14:26:58 +0200 Subject: [PATCH 1/2] Replace @public PHPDoc with #[Exposed] attribute Switch the strict-module public-API marker from a `@public` PHPDoc tag to a PHP 8 attribute `PhpModules\Attributes\Exposed`. Detection now reads class/enum attribute groups via PhpParser's NameResolver instead of parsing PHPDoc, dropping `DocReader::isPublic` (the import-level `@modules-ignore-next-line` stays as a comment since PHP attributes are not allowed on `use` statements). Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 8 +++---- modules.php | 11 +++++----- src/Attributes/Exposed.php | 10 +++++++++ src/Cli/Cli.php | 6 ++--- src/DocReader/DocReader.php | 22 ++----------------- src/Exceptions/PHPModulesException.php | 8 +++---- src/Lib/AnalysisResult.php | 5 ++--- src/Lib/Analyzer.php | 17 ++++++++++---- src/Lib/Domain/ClassDefinition.php | 2 +- src/Lib/Errors/Error.php | 6 ++--- src/Lib/Internal/DefinitionsGatherer.php | 20 +++++++++++++++-- src/Lib/Module.php | 5 ++--- src/Lib/Modules.php | 5 ++--- src/Lib/Reference.php | 4 ++-- src/Lib/SubModules.php | 6 ++--- tests/Lib/Analyzer/AnalyzerTestCase.php | 2 +- .../SectionA/SectionAModuleA/AAClass.php | 7 +++--- .../SectionA/SectionAModuleB/ABClass.php | 8 +++---- .../SectionB/SectionBModuleA/BAClass.php | 6 ++--- .../SectionB/SectionBModuleB/BBClass.php | 5 ++--- .../SectionA/SectionAModuleA/AAClass.php | 5 ++--- .../SectionA/SectionAModuleB/ABClass.php | 6 ++--- .../SectionB/SectionBModuleA/BAClass.php | 6 ++--- .../SectionB/SectionBModuleB/BBClass.php | 5 ++--- .../StrictPublic/Sample/ModuleA/ClassA.php | 5 ++--- .../StrictPublic/Sample/ModuleA/EnumA.php | 6 ++--- .../StrictPublic/Sample/ModuleB/ClassB.php | 7 +++--- .../StrictPublic/Sample/ModuleC/ClassC.php | 5 ++--- 28 files changed, 108 insertions(+), 100 deletions(-) create mode 100644 src/Attributes/Exposed.php diff --git a/README.md b/README.md index b90a704..679c78e 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,11 @@ $http = PhpModules\Lib\Module::strict('App\Http', [$persistence]); return Modules::builder('./src')->register([$persistence, $http]) ``` -Mark classes as public using PHPDoc: +Mark classes as public using the `#[Exposed]` attribute: ```php -/** - * @public - */ +use PhpModules\Attributes\Exposed; + +#[Exposed] class PersistedUser {} ``` diff --git a/modules.php b/modules.php index 41ef39d..8b0b821 100644 --- a/modules.php +++ b/modules.php @@ -18,12 +18,13 @@ $dependencies = [$phpparser, $phpdocparser, $graph, $graphviz, $symfonyConsole, $ciDetector]; /* Internal modules */ -$docreader = Module::strict('PhpModules\DocReader', [$phpdocparser]); -$exceptions = Module::strict('PhpModules\Exceptions'); -$lib = Module::strict('PhpModules\Lib', [$phpparser, $docreader, $exceptions]); -$cli = Module::strict('PhpModules\Cli', [$lib, $graph, $graphviz, $symfonyConsole, $ciDetector]); +$attributes = Module::create('PhpModules\Attributes'); +$docreader = Module::strict('PhpModules\DocReader', [$phpdocparser, $attributes]); +$exceptions = Module::strict('PhpModules\Exceptions', [$attributes]); +$lib = Module::strict('PhpModules\Lib', [$phpparser, $docreader, $exceptions, $attributes]); +$cli = Module::strict('PhpModules\Cli', [$lib, $graph, $graphviz, $symfonyConsole, $ciDetector, $attributes]); -$internal = [$docreader, $exceptions, $lib, $cli]; +$internal = [$attributes, $docreader, $exceptions, $lib, $cli]; return Modules::builder(__DIR__ . '/src') ->register($dependencies) diff --git a/src/Attributes/Exposed.php b/src/Attributes/Exposed.php new file mode 100644 index 0000000..e70366d --- /dev/null +++ b/src/Attributes/Exposed.php @@ -0,0 +1,10 @@ +prepare($phpdoc); - $lexer = new Lexer(); - $constExprParser = new ConstExprParser(); - $phpDocParser = new PhpDocParser(new TypeParser($constExprParser), $constExprParser); - $tokenize = $lexer->tokenize($phpdoc); - - $phpDocNode = $phpDocParser->parse(new TokenIterator($tokenize)); - - return count($phpDocNode->getTagsByName('@public')) > 0; - } - public function isIgnoredImport(?string $phpdoc): bool { if ($phpdoc === null) { diff --git a/src/Exceptions/PHPModulesException.php b/src/Exceptions/PHPModulesException.php index bfb1ae8..77ab6d6 100644 --- a/src/Exceptions/PHPModulesException.php +++ b/src/Exceptions/PHPModulesException.php @@ -2,10 +2,10 @@ namespace PhpModules\Exceptions; -/** - * @public - */ +use PhpModules\Attributes\Exposed; + +#[Exposed] class PHPModulesException extends \Exception { -} \ No newline at end of file +} diff --git a/src/Lib/AnalysisResult.php b/src/Lib/AnalysisResult.php index ad4a32d..396501e 100644 --- a/src/Lib/AnalysisResult.php +++ b/src/Lib/AnalysisResult.php @@ -2,11 +2,10 @@ namespace PhpModules\Lib; +use PhpModules\Attributes\Exposed; use PhpModules\Lib\Errors\Error; -/** - * @public - */ +#[Exposed] class AnalysisResult { /** diff --git a/src/Lib/Analyzer.php b/src/Lib/Analyzer.php index e75b65f..83e3aa5 100644 --- a/src/Lib/Analyzer.php +++ b/src/Lib/Analyzer.php @@ -2,6 +2,7 @@ namespace PhpModules\Lib; +use PhpModules\Attributes\Exposed; use PhpModules\DocReader\DocReader; use PhpModules\Exceptions\PHPModulesException; use PhpModules\Lib\Domain\ClassName; @@ -18,9 +19,7 @@ use PhpModules\Lib\Internal\SingleDependency; use ReflectionClass; -/** - * @public - */ +#[Exposed] class Analyzer { /** @@ -93,6 +92,16 @@ private function getErrors(FileDefinition $fileDefinition, Importable $import, a return []; } + // The Exposed attribute is a framework marker; importing it is always allowed, + // but if its module is registered we still mark the dependency as used. + if ((string)$import === Exposed::class) { + $moduleOfImport = $this->getModule($import); + if ($moduleOfImport !== null && $moduleOfImport !== $moduleOfFile) { + $this->markUsed($moduleOfFile, $moduleOfImport, $allDependencies); + } + return []; + } + // See if import is part of a module // if allowed to import undefined modules no errors $moduleOfImport = $this->getModule($import); @@ -151,7 +160,7 @@ private function isMarkedAsPublic(Importable $import): bool foreach ($this->fileDefinitions as $definition) { foreach ($definition->classDefinitions as $classDefinition) { if ($classDefinition->className->isEqual($import)) { - return $this->docReader->isPublic($classDefinition->phpdoc); + return $classDefinition->isExposed; } } } diff --git a/src/Lib/Domain/ClassDefinition.php b/src/Lib/Domain/ClassDefinition.php index 7937ddf..c51f155 100644 --- a/src/Lib/Domain/ClassDefinition.php +++ b/src/Lib/Domain/ClassDefinition.php @@ -9,7 +9,7 @@ class ClassDefinition { public bool $isEnum; - public function __construct(public ClassName $className, public ?string $phpdoc, bool $isEnum = false) + public function __construct(public ClassName $className, public bool $isExposed, bool $isEnum = false) { $this->isEnum = $isEnum; } diff --git a/src/Lib/Errors/Error.php b/src/Lib/Errors/Error.php index 05e60f7..9622755 100644 --- a/src/Lib/Errors/Error.php +++ b/src/Lib/Errors/Error.php @@ -2,9 +2,9 @@ namespace PhpModules\Lib\Errors; -/** - * @public - */ +use PhpModules\Attributes\Exposed; + +#[Exposed] abstract class Error { diff --git a/src/Lib/Internal/DefinitionsGatherer.php b/src/Lib/Internal/DefinitionsGatherer.php index 6aaaac3..2530de2 100644 --- a/src/Lib/Internal/DefinitionsGatherer.php +++ b/src/Lib/Internal/DefinitionsGatherer.php @@ -6,9 +6,12 @@ use PhpModules\Lib\Domain\ClassName; use PhpModules\Lib\Domain\FileDefinition; use PhpModules\Lib\Domain\Importable; +use PhpModules\Attributes\Exposed; use PhpModules\Lib\Domain\NamespaceName; use PhpModules\Lib\Modules; +use PhpParser\Node; use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\NameResolver; use PhpParser\ParserFactory; use SplFileInfo; @@ -25,6 +28,7 @@ public function gather(): array { $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); $traverser = new NodeTraverser; + $traverser->addVisitor(new NameResolver()); $recursiveIteratorIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->modules->path)); $regexIterator = new \RegexIterator($recursiveIteratorIterator, '/\.php$/'); @@ -81,7 +85,7 @@ public function gather(): array if ($classStmt->name !== null) { $classDefinition = new ClassDefinition( ClassName::fromNamespaceAndClassName($namespace, $classStmt->name), - $classStmt->getDocComment()?->getText() + $this->hasExposedAttribute($classStmt) ); $classDefinitions[] = $classDefinition; } @@ -91,7 +95,7 @@ public function gather(): array if ($enumStmt->name !== null) { $enumDefinition = new ClassDefinition( ClassName::fromNamespaceAndClassName($namespace, $enumStmt->name), - $enumStmt->getDocComment()?->getText(), + $this->hasExposedAttribute($enumStmt), true ); $classDefinitions[] = $enumDefinition; @@ -126,4 +130,16 @@ private function isIgnored(SplFileInfo $file): bool return false; } + private function hasExposedAttribute(Node\Stmt\Class_|Node\Stmt\Enum_ $stmt): bool + { + foreach ($stmt->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() === Exposed::class) { + return true; + } + } + } + return false; + } + } diff --git a/src/Lib/Module.php b/src/Lib/Module.php index 85006a2..491f72a 100644 --- a/src/Lib/Module.php +++ b/src/Lib/Module.php @@ -2,11 +2,10 @@ namespace PhpModules\Lib; +use PhpModules\Attributes\Exposed; use PhpModules\Lib\Domain\NamespaceName; -/** - * @public - */ +#[Exposed] class Module { diff --git a/src/Lib/Modules.php b/src/Lib/Modules.php index a762b45..f78a143 100644 --- a/src/Lib/Modules.php +++ b/src/Lib/Modules.php @@ -2,11 +2,10 @@ namespace PhpModules\Lib; +use PhpModules\Attributes\Exposed; use PhpModules\Exceptions\PHPModulesException; -/** - * @public - */ +#[Exposed] class Modules { /** diff --git a/src/Lib/Reference.php b/src/Lib/Reference.php index 239913c..70b3303 100644 --- a/src/Lib/Reference.php +++ b/src/Lib/Reference.php @@ -2,15 +2,15 @@ namespace PhpModules\Lib; +use PhpModules\Attributes\Exposed; use PhpModules\Lib\Domain\NamespaceName; /** - * @public - * * A reference refers to a module that you depend on but don't have access to the full module definition. * * This is useful when you want to depend on a module that is defined in a different submodule. */ +#[Exposed] class Reference { diff --git a/src/Lib/SubModules.php b/src/Lib/SubModules.php index 8e812f3..5d7f51c 100644 --- a/src/Lib/SubModules.php +++ b/src/Lib/SubModules.php @@ -2,9 +2,9 @@ namespace PhpModules\Lib; -/** - * @public - */ +use PhpModules\Attributes\Exposed; + +#[Exposed] class SubModules { diff --git a/tests/Lib/Analyzer/AnalyzerTestCase.php b/tests/Lib/Analyzer/AnalyzerTestCase.php index ee8add2..3e2db48 100644 --- a/tests/Lib/Analyzer/AnalyzerTestCase.php +++ b/tests/Lib/Analyzer/AnalyzerTestCase.php @@ -43,7 +43,7 @@ protected function file(string $namespace, array $classes, array $imports = []): $classDefinitions[] = $class; } if (is_string($class)) { - $classDefinitions[] = new ClassDefinition(ClassName::fromString($class), null); + $classDefinitions[] = new ClassDefinition(ClassName::fromString($class), false); } } diff --git a/tests/Lib/Analyzer/Project/SampleA/SectionA/SectionAModuleA/AAClass.php b/tests/Lib/Analyzer/Project/SampleA/SectionA/SectionAModuleA/AAClass.php index 36f4897..7f61c92 100644 --- a/tests/Lib/Analyzer/Project/SampleA/SectionA/SectionAModuleA/AAClass.php +++ b/tests/Lib/Analyzer/Project/SampleA/SectionA/SectionAModuleA/AAClass.php @@ -2,11 +2,10 @@ namespace Sample\SectionA\SectionAModuleA; +use PhpModules\Attributes\Exposed; use Sample\SectionA\SectionAModuleB\ABClass; -/** - * @public - */ +#[Exposed] class AAClass { @@ -15,4 +14,4 @@ public function get(): ABClass return new ABClass(); } -} \ No newline at end of file +} diff --git a/tests/Lib/Analyzer/Project/SampleA/SectionA/SectionAModuleB/ABClass.php b/tests/Lib/Analyzer/Project/SampleA/SectionA/SectionAModuleB/ABClass.php index f077458..c279fee 100644 --- a/tests/Lib/Analyzer/Project/SampleA/SectionA/SectionAModuleB/ABClass.php +++ b/tests/Lib/Analyzer/Project/SampleA/SectionA/SectionAModuleB/ABClass.php @@ -2,10 +2,10 @@ namespace Sample\SectionA\SectionAModuleB; -/** - * @public - */ +use PhpModules\Attributes\Exposed; + +#[Exposed] class ABClass { -} \ No newline at end of file +} diff --git a/tests/Lib/Analyzer/Project/SampleA/SectionB/SectionBModuleA/BAClass.php b/tests/Lib/Analyzer/Project/SampleA/SectionB/SectionBModuleA/BAClass.php index c3df6fa..5241dd4 100644 --- a/tests/Lib/Analyzer/Project/SampleA/SectionB/SectionBModuleA/BAClass.php +++ b/tests/Lib/Analyzer/Project/SampleA/SectionB/SectionBModuleA/BAClass.php @@ -2,9 +2,9 @@ namespace Sample\SectionB\SectionBModuleA; -/** - * @public - */ +use PhpModules\Attributes\Exposed; + +#[Exposed] class BAClass { diff --git a/tests/Lib/Analyzer/Project/SampleA/SectionB/SectionBModuleB/BBClass.php b/tests/Lib/Analyzer/Project/SampleA/SectionB/SectionBModuleB/BBClass.php index b37a29b..1cda305 100644 --- a/tests/Lib/Analyzer/Project/SampleA/SectionB/SectionBModuleB/BBClass.php +++ b/tests/Lib/Analyzer/Project/SampleA/SectionB/SectionBModuleB/BBClass.php @@ -2,11 +2,10 @@ namespace Sample\SectionB\SectionBModuleB; +use PhpModules\Attributes\Exposed; use Sample\SectionA\SectionAModuleB\ABClass; -/** - * @public - */ +#[Exposed] class BBClass { diff --git a/tests/Lib/Analyzer/Project/SampleB/SectionA/SectionAModuleA/AAClass.php b/tests/Lib/Analyzer/Project/SampleB/SectionA/SectionAModuleA/AAClass.php index 36f4897..3b93ab0 100644 --- a/tests/Lib/Analyzer/Project/SampleB/SectionA/SectionAModuleA/AAClass.php +++ b/tests/Lib/Analyzer/Project/SampleB/SectionA/SectionAModuleA/AAClass.php @@ -2,11 +2,10 @@ namespace Sample\SectionA\SectionAModuleA; +use PhpModules\Attributes\Exposed; use Sample\SectionA\SectionAModuleB\ABClass; -/** - * @public - */ +#[Exposed] class AAClass { diff --git a/tests/Lib/Analyzer/Project/SampleB/SectionA/SectionAModuleB/ABClass.php b/tests/Lib/Analyzer/Project/SampleB/SectionA/SectionAModuleB/ABClass.php index f077458..df200c1 100644 --- a/tests/Lib/Analyzer/Project/SampleB/SectionA/SectionAModuleB/ABClass.php +++ b/tests/Lib/Analyzer/Project/SampleB/SectionA/SectionAModuleB/ABClass.php @@ -2,9 +2,9 @@ namespace Sample\SectionA\SectionAModuleB; -/** - * @public - */ +use PhpModules\Attributes\Exposed; + +#[Exposed] class ABClass { diff --git a/tests/Lib/Analyzer/Project/SampleB/SectionB/SectionBModuleA/BAClass.php b/tests/Lib/Analyzer/Project/SampleB/SectionB/SectionBModuleA/BAClass.php index c3df6fa..5241dd4 100644 --- a/tests/Lib/Analyzer/Project/SampleB/SectionB/SectionBModuleA/BAClass.php +++ b/tests/Lib/Analyzer/Project/SampleB/SectionB/SectionBModuleA/BAClass.php @@ -2,9 +2,9 @@ namespace Sample\SectionB\SectionBModuleA; -/** - * @public - */ +use PhpModules\Attributes\Exposed; + +#[Exposed] class BAClass { diff --git a/tests/Lib/Analyzer/Project/SampleB/SectionB/SectionBModuleB/BBClass.php b/tests/Lib/Analyzer/Project/SampleB/SectionB/SectionBModuleB/BBClass.php index c5b77b6..a6e80c3 100644 --- a/tests/Lib/Analyzer/Project/SampleB/SectionB/SectionBModuleB/BBClass.php +++ b/tests/Lib/Analyzer/Project/SampleB/SectionB/SectionBModuleB/BBClass.php @@ -2,11 +2,10 @@ namespace Sample\SectionB\SectionBModuleB; +use PhpModules\Attributes\Exposed; use Sample\SectionA\SectionAModuleA\AAClass; -/** - * @public - */ +#[Exposed] class BBClass { diff --git a/tests/Lib/Analyzer/StrictPublic/Sample/ModuleA/ClassA.php b/tests/Lib/Analyzer/StrictPublic/Sample/ModuleA/ClassA.php index 3fd2710..85f1bc2 100644 --- a/tests/Lib/Analyzer/StrictPublic/Sample/ModuleA/ClassA.php +++ b/tests/Lib/Analyzer/StrictPublic/Sample/ModuleA/ClassA.php @@ -2,11 +2,10 @@ namespace Sample\ModuleA; +use PhpModules\Attributes\Exposed; use Sample\ModuleA\Internal\InternalClassA; -/** - * @public - */ +#[Exposed] class ClassA { diff --git a/tests/Lib/Analyzer/StrictPublic/Sample/ModuleA/EnumA.php b/tests/Lib/Analyzer/StrictPublic/Sample/ModuleA/EnumA.php index 34fa009..5d3ab05 100644 --- a/tests/Lib/Analyzer/StrictPublic/Sample/ModuleA/EnumA.php +++ b/tests/Lib/Analyzer/StrictPublic/Sample/ModuleA/EnumA.php @@ -2,9 +2,9 @@ namespace Sample\ModuleA; -/** - * @public - */ +use PhpModules\Attributes\Exposed; + +#[Exposed] enum EnumA { case VALUE1; diff --git a/tests/Lib/Analyzer/StrictPublic/Sample/ModuleB/ClassB.php b/tests/Lib/Analyzer/StrictPublic/Sample/ModuleB/ClassB.php index fd16d9c..a0493b2 100644 --- a/tests/Lib/Analyzer/StrictPublic/Sample/ModuleB/ClassB.php +++ b/tests/Lib/Analyzer/StrictPublic/Sample/ModuleB/ClassB.php @@ -2,12 +2,11 @@ namespace Sample\ModuleB; +use PhpModules\Attributes\Exposed; use Sample\ModuleA\ClassA; use Sample\ModuleA\Internal\InternalClassA; -/** - * @public - */ +#[Exposed] class ClassB { @@ -16,7 +15,7 @@ public function run(): void $classA = new ClassA(); $classA->run(); - //This isn't allowed since it is not annotated with @public + //This isn't allowed since it is not annotated with #[Exposed] $internalClassA = new InternalClassA(); $internalClassA->run(); } diff --git a/tests/Lib/Analyzer/StrictPublic/Sample/ModuleC/ClassC.php b/tests/Lib/Analyzer/StrictPublic/Sample/ModuleC/ClassC.php index 93cd412..92231e5 100644 --- a/tests/Lib/Analyzer/StrictPublic/Sample/ModuleC/ClassC.php +++ b/tests/Lib/Analyzer/StrictPublic/Sample/ModuleC/ClassC.php @@ -2,11 +2,10 @@ namespace Sample\ModuleC; +use PhpModules\Attributes\Exposed; use Sample\ModuleC\Internal\InternalClassC; -/** - * @public - */ +#[Exposed] class ClassC { From 7597ee4b60beb8157bdeb3ffaa6a36c88719e485 Mon Sep 17 00:00:00 2001 From: Joris Blaak Date: Sat, 25 Apr 2026 14:36:20 +0200 Subject: [PATCH 2/2] Add global ignore list for framework attributes and built-ins The Analyzer now skips imports that match a hardcoded list of namespaces (PhpModules\Attributes for the framework's own attributes, Closure for the PHP built-in) before any module resolution. This removes the need to register PhpModules\Attributes as a dependency on every module in modules.php, and replaces the previous Exposed-specific bypass with a general mechanism. Co-Authored-By: Claude Opus 4.7 (1M context) --- modules.php | 11 +++++------ src/Lib/Analyzer.php | 34 ++++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/modules.php b/modules.php index 8b0b821..41ef39d 100644 --- a/modules.php +++ b/modules.php @@ -18,13 +18,12 @@ $dependencies = [$phpparser, $phpdocparser, $graph, $graphviz, $symfonyConsole, $ciDetector]; /* Internal modules */ -$attributes = Module::create('PhpModules\Attributes'); -$docreader = Module::strict('PhpModules\DocReader', [$phpdocparser, $attributes]); -$exceptions = Module::strict('PhpModules\Exceptions', [$attributes]); -$lib = Module::strict('PhpModules\Lib', [$phpparser, $docreader, $exceptions, $attributes]); -$cli = Module::strict('PhpModules\Cli', [$lib, $graph, $graphviz, $symfonyConsole, $ciDetector, $attributes]); +$docreader = Module::strict('PhpModules\DocReader', [$phpdocparser]); +$exceptions = Module::strict('PhpModules\Exceptions'); +$lib = Module::strict('PhpModules\Lib', [$phpparser, $docreader, $exceptions]); +$cli = Module::strict('PhpModules\Cli', [$lib, $graph, $graphviz, $symfonyConsole, $ciDetector]); -$internal = [$attributes, $docreader, $exceptions, $lib, $cli]; +$internal = [$docreader, $exceptions, $lib, $cli]; return Modules::builder(__DIR__ . '/src') ->register($dependencies) diff --git a/src/Lib/Analyzer.php b/src/Lib/Analyzer.php index 83e3aa5..b72dcdd 100644 --- a/src/Lib/Analyzer.php +++ b/src/Lib/Analyzer.php @@ -22,6 +22,15 @@ #[Exposed] class Analyzer { + /** + * Imports that are always allowed regardless of module configuration. + * Matched as namespace parents, so any class under these namespaces is ignored. + */ + private const GLOBAL_IGNORED_NAMESPACES = [ + 'PhpModules\\Attributes', + 'Closure', + ]; + /** * @param Modules $modules * @param FileDefinition[] $fileDefinitions @@ -85,6 +94,11 @@ private function getErrors(FileDefinition $fileDefinition, Importable $import, a return []; } + // Globally ignored imports (framework attributes, built-in classes) are always allowed + if ($this->isGloballyIgnored($import)) { + return []; + } + // Check if file is part of some module, if not, no errors $moduleOfFile = $this->findModule($fileDefinition); @@ -92,16 +106,6 @@ private function getErrors(FileDefinition $fileDefinition, Importable $import, a return []; } - // The Exposed attribute is a framework marker; importing it is always allowed, - // but if its module is registered we still mark the dependency as used. - if ((string)$import === Exposed::class) { - $moduleOfImport = $this->getModule($import); - if ($moduleOfImport !== null && $moduleOfImport !== $moduleOfFile) { - $this->markUsed($moduleOfFile, $moduleOfImport, $allDependencies); - } - return []; - } - // See if import is part of a module // if allowed to import undefined modules no errors $moduleOfImport = $this->getModule($import); @@ -254,6 +258,16 @@ function ($className) { return in_array((string)$import, $internalDefinedClasses); } + private function isGloballyIgnored(Importable $import): bool + { + foreach (self::GLOBAL_IGNORED_NAMESPACES as $ignoredNamespace) { + if (NamespaceName::fromString($ignoredNamespace)->isParentOf($import)) { + return true; + } + } + return false; + } + /** * @param Reference $dependency * @param Module[]|Reference[] $modules