From c30cdb2e0fcf3d7cc9dc77c865163a60c3d17b49 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 6 Nov 2026 20:11:19 +0700 Subject: [PATCH 1/2] chore: Bump StructArmed to ^0.21 and use ClassNode signatures via SignatureTypesAwareRuleInterface --- tools/structarmed/composer.json | 2 +- ...rnalMustNotLeakIntoPublicSignatureRule.php | 47 +++++-- .../src/Support/ClassSignatures.php | 117 ------------------ 3 files changed, 40 insertions(+), 126 deletions(-) delete mode 100644 tools/structarmed/src/Support/ClassSignatures.php diff --git a/tools/structarmed/composer.json b/tools/structarmed/composer.json index d148bae6..3cb5f8a8 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.0" }, "autoload": { "psr-4": { diff --git a/tools/structarmed/src/Rule/InternalMustNotLeakIntoPublicSignatureRule.php b/tools/structarmed/src/Rule/InternalMustNotLeakIntoPublicSignatureRule.php index 679fe1c8..8fce96f5 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\\'; @@ -42,21 +42,27 @@ public function evaluate(ClassNode $classNode): ?RuleViolation public function evaluateAll(ClassNode $classNode): array { $violations = []; + $privatelyPromoted = $this->privatelyPromotedParams($classNode); - foreach (ClassSignatures::publicTypes($classNode->file, $classNode->shortName()) as $type) { - if (! \str_contains($type, self::SEGMENT)) { + foreach ($classNode->signatures as $signature) { + if (! $signature->isExposed() + || ! \str_contains($signature->type, self::SEGMENT) + || isset($privatelyPromoted[self::key($signature)]) + ) { 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, ); @@ -65,6 +71,31 @@ className: $classNode->className, return $violations; } + /** + * StructArmed records a privately promoted constructor param both as a private + * property and as a param of the constructor. It is a private property, not + * public surface, so the param entry sharing its name, type and line is exempt. + * + * @return array + */ + private function privatelyPromotedParams(ClassNode $classNode): array + { + $keys = []; + + foreach ($classNode->signatures as $signature) { + if ($signature->kind === ClassSignature::PROPERTY && ! $signature->isExposed()) { + $keys[self::key($signature)] = true; + } + } + + return $keys; + } + + private static function key(ClassSignature $signature): string + { + return "{$signature->name}|{$signature->type}|{$signature->line}"; + } + private function isInternal(ClassNode $classNode): bool { return \str_contains($classNode->className, self::SEGMENT) 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. - } - } -} From 231674cbd5291f9cae4c1e5085f4ddb892fa533d Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 6 Nov 2026 20:44:36 +0700 Subject: [PATCH 2/2] update to ^0.21.2 and make use of isPromoted --- tools/structarmed/composer.json | 2 +- ...rnalMustNotLeakIntoPublicSignatureRule.php | 29 ++----------------- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/tools/structarmed/composer.json b/tools/structarmed/composer.json index 3cb5f8a8..19a432a5 100644 --- a/tools/structarmed/composer.json +++ b/tools/structarmed/composer.json @@ -1,6 +1,6 @@ { "require": { - "boundwize/structarmed": "^0.21.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 8fce96f5..c06b735d 100644 --- a/tools/structarmed/src/Rule/InternalMustNotLeakIntoPublicSignatureRule.php +++ b/tools/structarmed/src/Rule/InternalMustNotLeakIntoPublicSignatureRule.php @@ -42,12 +42,12 @@ public function evaluate(ClassNode $classNode): ?RuleViolation public function evaluateAll(ClassNode $classNode): array { $violations = []; - $privatelyPromoted = $this->privatelyPromotedParams($classNode); 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) - || isset($privatelyPromoted[self::key($signature)]) ) { continue; } @@ -71,31 +71,6 @@ className: $classNode->className, return $violations; } - /** - * StructArmed records a privately promoted constructor param both as a private - * property and as a param of the constructor. It is a private property, not - * public surface, so the param entry sharing its name, type and line is exempt. - * - * @return array - */ - private function privatelyPromotedParams(ClassNode $classNode): array - { - $keys = []; - - foreach ($classNode->signatures as $signature) { - if ($signature->kind === ClassSignature::PROPERTY && ! $signature->isExposed()) { - $keys[self::key($signature)] = true; - } - } - - return $keys; - } - - private static function key(ClassSignature $signature): string - { - return "{$signature->name}|{$signature->type}|{$signature->line}"; - } - private function isInternal(ClassNode $classNode): bool { return \str_contains($classNode->className, self::SEGMENT)