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/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..b72dcdd 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,11 +19,18 @@ use PhpModules\Lib\Internal\SingleDependency; use ReflectionClass; -/** - * @public - */ +#[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 @@ -86,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); @@ -151,7 +164,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; } } } @@ -245,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 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 {