diff --git a/tools/structarmed/composer.json b/tools/structarmed/composer.json index d148bae6..19a432a5 100644 --- a/tools/structarmed/composer.json +++ b/tools/structarmed/composer.json @@ -1,6 +1,6 @@ { "require": { - "boundwize/structarmed": "^0.20.0" + "boundwize/structarmed": "^0.21.2" }, "autoload": { "psr-4": { diff --git a/tools/structarmed/src/Rule/InternalMustNotLeakIntoPublicSignatureRule.php b/tools/structarmed/src/Rule/InternalMustNotLeakIntoPublicSignatureRule.php index 679fe1c8..c06b735d 100644 --- a/tools/structarmed/src/Rule/InternalMustNotLeakIntoPublicSignatureRule.php +++ b/tools/structarmed/src/Rule/InternalMustNotLeakIntoPublicSignatureRule.php @@ -5,10 +5,10 @@ namespace Testo\Tools\StructArmed\Rule; use Boundwize\StructArmed\Analyser\ClassNode; +use Boundwize\StructArmed\Analyser\ClassSignature; use Boundwize\StructArmed\Rule\MultipleRuleViolationInterface; -use Boundwize\StructArmed\Rule\RuleInterface; use Boundwize\StructArmed\Rule\RuleViolation; -use Testo\Tools\StructArmed\Support\ClassSignatures; +use Boundwize\StructArmed\Rule\SignatureTypesAwareRuleInterface; /** * A class on the public surface must not name an internal type in its signature. @@ -18,7 +18,7 @@ * exempt: internal-to-internal coupling stays behind the package boundary (and any * cross-package leak is caught by [InternalIsUsableOnlyWithinItsPackageRule]). */ -final readonly class InternalMustNotLeakIntoPublicSignatureRule implements RuleInterface, MultipleRuleViolationInterface +final readonly class InternalMustNotLeakIntoPublicSignatureRule implements SignatureTypesAwareRuleInterface, MultipleRuleViolationInterface { private const SEGMENT = '\\Internal\\'; @@ -43,20 +43,26 @@ public function evaluateAll(ClassNode $classNode): array { $violations = []; - foreach (ClassSignatures::publicTypes($classNode->file, $classNode->shortName()) as $type) { - if (! \str_contains($type, self::SEGMENT)) { + foreach ($classNode->signatures as $signature) { + // A privately promoted constructor param is a private property, not public surface. + if (! $signature->isExposed() + || ($signature->kind === ClassSignature::PARAM && $signature->isPromoted) + || ! \str_contains($signature->type, self::SEGMENT) + ) { continue; } $violations[] = new RuleViolation( message: \sprintf( - '%s [%s] exposes internal type [%s] in its public signature', + '%s [%s] exposes internal type [%s] in its public signature through %s [%s]', $classNode->getType(), $classNode->className, - $type, + $signature->type, + $signature->kind, + $signature->name, ), file: $classNode->file, - line: $classNode->line, + line: $signature->line, className: $classNode->className, layer: $classNode->layer, ); diff --git a/tools/structarmed/src/Support/ClassSignatures.php b/tools/structarmed/src/Support/ClassSignatures.php deleted file mode 100644 index a5c01c4d..00000000 --- a/tools/structarmed/src/Support/ClassSignatures.php +++ /dev/null @@ -1,117 +0,0 @@ ->> file => short class name => FQ type names on its public surface */ - private static array $cache = []; - - /** - * @return list - */ - public static function publicTypes(string $file, string $shortName): array - { - return (self::$cache[$file] ??= self::parse($file))[$shortName] ?? []; - } - - /** - * @return array> - */ - private static function parse(string $file): array - { - $code = @\file_get_contents($file); - - if ($code === false) { - return []; - } - - try { - $ast = (new ParserFactory())->createForNewestSupportedVersion()->parse($code) ?? []; - - $traverser = new NodeTraverser(new NameResolver()); - $ast = $traverser->traverse($ast); - } catch (\Throwable) { - return []; - } - - $signatures = []; - - foreach ((new NodeFinder())->findInstanceOf($ast, ClassLike::class) as $class) { - $name = $class->name?->toString(); - - if ($name === null) { - continue; - } - - $types = []; - - foreach ($class->getMethods() as $method) { - if ($method->isPrivate()) { - continue; - } - - foreach ($method->params as $param) { - // A privately promoted constructor param is a private property, not surface. - ($param->flags & Modifiers::PRIVATE) === 0 and self::collect($param->type, $types); - } - - self::collect($method->returnType, $types); - } - - foreach ($class->getProperties() as $property) { - $property->isPrivate() or self::collect($property->type, $types); - } - - $signatures[$name] = \array_values(\array_unique($types)); - } - - return $signatures; - } - - /** - * @param list $types - */ - private static function collect(?Node $type, array &$types): void - { - switch (true) { - case $type instanceof Name: - $types[] = $type->toString(); - break; - case $type instanceof NullableType: - self::collect($type->type, $types); - break; - case $type instanceof UnionType: - case $type instanceof IntersectionType: - foreach ($type->types as $inner) { - self::collect($inner, $types); - } - break; - // A plain Identifier (int, string, void, self, static, ...) carries no namespace: ignore. - } - } -}