From 5cf3bc877d2f13572e0a1eb13734fbcbdf9e7900 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sun, 21 Jun 2026 05:50:36 +0700 Subject: [PATCH] Fix function rule suppression by class-like usage --- README.md | 16 +++++++++++++++- src/Rule/UsageRule.php | 2 +- tests/Analysis/UsageClassifierTest.php | 20 ++++++++++++++++++++ tests/Rule/UsageRuleTest.php | 12 ++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a1aee9..4561dc6 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,21 @@ return PyrameterConfig::create() | `MakesHttpRequests` | `functional` | | Both traits | `functional` | -The optional `unless` argument is also available on `usesNamespace()` and `usesFunction()`. +The optional `unless` argument is also available on `usesNamespace()` and `usesFunction()`. Its values always identify classes or traits, regardless of the rule type. For example, a filesystem function rule can be suppressed when a test uses a virtual-filesystem trait: + +```php +use App\Tests\Concerns\UsesVirtualFilesystem; + +return PyrameterConfig::create() + ->usesFunction( + 'file_put_contents', + TestKind::Integration, + unless: [UsesVirtualFilesystem::class], + ) + ->usesClass(UsesVirtualFilesystem::class, TestKind::Functional); +``` + +A test that calls `file_put_contents()` and uses `UsesVirtualFilesystem` is classified as `functional`. The equivalent CodeIgniter exception is already included in `defaults()`: diff --git a/src/Rule/UsageRule.php b/src/Rule/UsageRule.php index 964a014..6b41565 100644 --- a/src/Rule/UsageRule.php +++ b/src/Rule/UsageRule.php @@ -67,7 +67,7 @@ public function normalizedUnlessKeys(): array $normalizedUnlessKeys = []; foreach ($this->unless as $unlessUsage) { - $normalizedUnlessKeys[] = $this->usageKey($this->usageType, $this->normalize($unlessUsage)); + $normalizedUnlessKeys[] = $this->usageKey(UsageType::ClassLike, $this->normalize($unlessUsage)); } return $normalizedUnlessKeys; diff --git a/tests/Analysis/UsageClassifierTest.php b/tests/Analysis/UsageClassifierTest.php index 87d6217..a86f42e 100644 --- a/tests/Analysis/UsageClassifierTest.php +++ b/tests/Analysis/UsageClassifierTest.php @@ -224,6 +224,26 @@ public function testRuleCanBeSuppressedByAnotherConsumedUsage(): void $this->assertSame(TestKind::Integration, $usageClassifier->classify(['Framework\DatabaseTrait'])); } + public function testFunctionRuleCanBeSuppressedByClassLikeUsage(): void + { + $pyrameterConfig = PyrameterConfig::create() + ->usesFunction( + 'file_put_contents', + TestKind::Integration, + unless: [self::class], + ) + ->usesClass(self::class, TestKind::Functional); + $usageClassifier = new UsageClassifier($pyrameterConfig->usageRules()); + + $this->assertSame( + TestKind::Functional, + $usageClassifier->classify([ + 'function:file_put_contents', + 'class:' . self::class, + ]), + ); + } + public function testHeaviestMatchingRuleWinsAfterRulesArePrecompiled(): void { $usageClassifier = new UsageClassifier([ diff --git a/tests/Rule/UsageRuleTest.php b/tests/Rule/UsageRuleTest.php index 553bc5c..f9b8b25 100644 --- a/tests/Rule/UsageRuleTest.php +++ b/tests/Rule/UsageRuleTest.php @@ -72,4 +72,16 @@ public function testItNormalizesUnlessUsages(): void $this->assertSame(['class:framework\controllertrait'], $usageRule->normalizedUnlessKeys()); } + + public function testItNormalizesFunctionRuleUnlessUsagesAsClassLike(): void + { + $usageRule = new UsageRule( + 'file_put_contents', + TestKind::Integration, + UsageType::Function, + unless: ['\APP\Tests\UsesVirtualFilesystem'], + ); + + $this->assertSame(['class:app\tests\usesvirtualfilesystem'], $usageRule->normalizedUnlessKeys()); + } }