diff --git a/pyrameter.php b/pyrameter.php index a3b273a..7e69e7b 100644 --- a/pyrameter.php +++ b/pyrameter.php @@ -8,9 +8,9 @@ return PyrameterConfig::defaults() ->usesNamespace('Boundwize\\Pyrameter\\Event\\', TestKind::Functional) ->targetShape( - unit: ['min' => 60], - functional: ['max' => 20], + unit: ['min' => 76], + functional: ['max' => 8], integration: ['max' => 16], - e2e: ['max' => 2], + e2e: ['max' => 0], ) ->failOnViolation(); diff --git a/src/Analysis/UsageClassifier.php b/src/Analysis/UsageClassifier.php index a195523..6d312ce 100644 --- a/src/Analysis/UsageClassifier.php +++ b/src/Analysis/UsageClassifier.php @@ -11,33 +11,40 @@ use function array_keys; use function ltrim; use function sprintf; -use function str_contains; use function str_ends_with; use function str_starts_with; use function strlen; +use function strpos; use function strtolower; use function substr; final readonly class UsageClassifier { + /** @var array */ + private const USAGE_TYPES_BY_PREFIX = [ + 'class:' => UsageType::ClassLike, + 'function:' => UsageType::Function, + ]; + /** @var array}>> */ private array $exactRules; - /** @var list}> */ - private array $namespaceRules; + /** @var array}>> */ + private array $namespaceRulesByRoot; /** * @param list $rules */ public function __construct(array $rules) { - $exactRules = []; - $namespaceRules = []; + $exactRules = []; + $namespaceRulesByRoot = []; foreach ($rules as $rule) { if ($rule->isNamespaceRule()) { - $namespaceRules[] = [ - 'usage' => $rule->normalizedKey(), + $normalizedKey = $rule->normalizedKey(); + $namespaceRulesByRoot[$this->namespaceRoot($normalizedKey)][] = [ + 'usage' => $normalizedKey, 'kind' => $rule->kind, 'unless' => $rule->normalizedUnlessKeys(), ]; @@ -51,8 +58,8 @@ public function __construct(array $rules) ]; } - $this->exactRules = $exactRules; - $this->namespaceRules = $namespaceRules; + $this->exactRules = $exactRules; + $this->namespaceRulesByRoot = $namespaceRulesByRoot; } /** @@ -81,7 +88,9 @@ public function classify(array $consumedUsages): TestKind } } - foreach ($this->namespaceRules as $namespaceRule) { + $namespaceRoot = $this->namespaceRoot($normalizedConsumedUsage); + + foreach ($this->namespaceRulesByRoot[$namespaceRoot] ?? [] as $namespaceRule) { if ( ! $this->matchesNamespaceRule($normalizedConsumedUsage, $namespaceRule['usage']) || $this->isSuppressed($namespaceRule['unless'], $normalizedConsumedUsages) @@ -127,21 +136,31 @@ private function matchesNamespaceRule(string $consumedUsage, string $namespaceUs && str_starts_with($consumedUsage, $namespaceUsage); } + private function namespaceRoot(string $normalizedUsage): string + { + $separatorPosition = strpos($normalizedUsage, '\\'); + + return $separatorPosition === false + ? $normalizedUsage + : substr($normalizedUsage, 0, $separatorPosition + 1); + } + private function normalizeConsumedUsage(string $consumedUsage): string { - if (! str_contains($consumedUsage, ':')) { + $separatorPosition = strpos($consumedUsage, ':'); + + if ($separatorPosition === false) { return $this->usageKey(UsageType::ClassLike, $this->normalizeUsageName($consumedUsage)); } - foreach (UsageType::cases() as $usageType) { - $prefix = $usageType->value . ':'; + $prefix = substr($consumedUsage, 0, $separatorPosition + 1); + $usageType = self::USAGE_TYPES_BY_PREFIX[$prefix] ?? null; - if (str_starts_with($consumedUsage, $prefix)) { - return $this->usageKey( - $usageType, - $this->normalizeUsageName(substr($consumedUsage, strlen($prefix))) - ); - } + if ($usageType !== null) { + return $this->usageKey( + $usageType, + $this->normalizeUsageName(substr($consumedUsage, $separatorPosition + 1)) + ); } return $this->usageKey(UsageType::ClassLike, $this->normalizeUsageName($consumedUsage)); diff --git a/src/Detection/ConsumedUsageVisitor.php b/src/Detection/ConsumedUsageVisitor.php index 6e06b4d..d16f148 100644 --- a/src/Detection/ConsumedUsageVisitor.php +++ b/src/Detection/ConsumedUsageVisitor.php @@ -31,7 +31,6 @@ use PhpParser\NodeVisitorAbstract; use function array_keys; -use function in_array; use function ltrim; use function sprintf; use function strtolower; @@ -41,13 +40,20 @@ final class ConsumedUsageVisitor extends NodeVisitorAbstract /** @var array */ private array $consumedUsages = []; - /** @var list */ - private array $mockMethods = [ - 'createMock', - 'createStub', - 'createConfiguredMock', - 'createPartialMock', - 'getMockBuilder', + /** @var array */ + private const MOCK_METHODS = [ + 'createMock' => true, + 'createStub' => true, + 'createConfiguredMock' => true, + 'createPartialMock' => true, + 'getMockBuilder' => true, + ]; + + /** @var array */ + private const RESERVED_CLASS_NAMES = [ + 'self' => true, + 'static' => true, + 'parent' => true, ]; public function enterNode(Node $node): null @@ -189,7 +195,7 @@ private function addUsage(string $usage): void { $usage = ltrim($usage, '\\'); - if ($usage === '' || in_array(strtolower($usage), ['self', 'static', 'parent'], true)) { + if ($usage === '' || isset(self::RESERVED_CLASS_NAMES[strtolower($usage)])) { return; } @@ -209,7 +215,7 @@ private function isClassConstant(ClassConstFetch $classConstFetch): bool private function markMockTarget(MethodCall|StaticCall $call): void { - if (! $call->name instanceof Identifier || ! in_array($call->name->toString(), $this->mockMethods, true)) { + if (! $call->name instanceof Identifier || ! isset(self::MOCK_METHODS[$call->name->toString()])) { return; } diff --git a/src/Rule/UsageRule.php b/src/Rule/UsageRule.php index 6b41565..587ee96 100644 --- a/src/Rule/UsageRule.php +++ b/src/Rule/UsageRule.php @@ -17,6 +17,11 @@ final readonly class UsageRule { + private string $normalizedUsage; + + /** @var list */ + private array $normalizedUnlessKeys; + /** * @param list $unless */ @@ -24,13 +29,24 @@ public function __construct( public string $usage, public TestKind $kind, private UsageType $usageType = UsageType::ClassLike, - private array $unless = [], + array $unless = [], ) { + $normalizedUnlessKeys = []; + + foreach ($unless as $unlessUsage) { + $normalizedUnlessKeys[] = $this->usageKey( + UsageType::ClassLike, + $this->normalize($unlessUsage), + ); + } + + $this->normalizedUsage = $this->normalize($usage); + $this->normalizedUnlessKeys = $normalizedUnlessKeys; } public function matches(string $consumedUsage): bool { - $configuredUsage = $this->normalizedUsage(); + $configuredUsage = $this->normalizedUsage; [$consumedUsageType, $consumedUsage] = $this->normalizeConsumedUsage($consumedUsage); if ($consumedUsageType !== $this->usageType) { @@ -44,19 +60,14 @@ public function matches(string $consumedUsage): bool return $this->matchesNamespacePrefix($consumedUsage, $configuredUsage); } - public function normalizedUsage(): string - { - return $this->normalize($this->usage); - } - public function isNamespaceRule(): bool { - return $this->usageType === UsageType::ClassLike && str_ends_with($this->normalizedUsage(), '\\'); + return $this->usageType === UsageType::ClassLike && str_ends_with($this->normalizedUsage, '\\'); } public function normalizedKey(): string { - return $this->usageKey($this->usageType, $this->normalizedUsage()); + return $this->usageKey($this->usageType, $this->normalizedUsage); } /** @@ -64,13 +75,7 @@ public function normalizedKey(): string */ public function normalizedUnlessKeys(): array { - $normalizedUnlessKeys = []; - - foreach ($this->unless as $unlessUsage) { - $normalizedUnlessKeys[] = $this->usageKey(UsageType::ClassLike, $this->normalize($unlessUsage)); - } - - return $normalizedUnlessKeys; + return $this->normalizedUnlessKeys; } private function normalize(string $usage): string