From cf8fac1a1ba7db5faf7f22bf8c646669acc2ae58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Mon, 2 Oct 2023 17:22:29 +0200 Subject: [PATCH 1/3] feat: add ShouldHaveOnlyOnePublicMethod rule --- docs/documentation/assertions.md | 3 ++ extension.neon | 6 +++ .../HasOnlyOnePublicMethodRule.php | 10 ++++ .../ShouldHaveOnlyOnePublicMethod.php | 41 ++++++++++++++++ .../Declaration/OnePublicMethodExtractor.php | 29 +++++++++++ src/Test/Builder/AssertionStep.php | 8 +++ .../Special/ClassWithOnePublicMethod.php | 13 +++++ .../ClassWithOnlyOnePublicMethodTest.php | 49 +++++++++++++++++++ ...tationClassWithOnlyOnePublicMethodTest.php | 48 ++++++++++++++++++ 9 files changed, 207 insertions(+) create mode 100644 src/Rule/Assertion/Declaration/ShouldHaveOnlyOnePublicMethod/HasOnlyOnePublicMethodRule.php create mode 100644 src/Rule/Assertion/Declaration/ShouldHaveOnlyOnePublicMethod/ShouldHaveOnlyOnePublicMethod.php create mode 100644 src/Rule/Extractor/Declaration/OnePublicMethodExtractor.php create mode 100644 tests/fixtures/Special/ClassWithOnePublicMethod.php create mode 100644 tests/unit/rules/ShouldHaveOnlyOnePublicMethod/ClassWithOnlyOnePublicMethodTest.php create mode 100644 tests/unit/rules/ShouldHaveOnlyOnePublicMethod/GoodImplementationClassWithOnlyOnePublicMethodTest.php diff --git a/docs/documentation/assertions.md b/docs/documentation/assertions.md index bca2925b..92707aac 100644 --- a/docs/documentation/assertions.md +++ b/docs/documentation/assertions.md @@ -35,3 +35,6 @@ It asserts that the selected classes **do not use the constructor** of the targe It asserts that the selected classes **does not depend** on anything else than the target classes. This would be equivalent to `shouldNotDependOn()` with the negation of the target classes. + +## shouldHaveOnlyOnePublicMethod() +It asserts that the selected classes **only have one public method**. diff --git a/extension.neon b/extension.neon index 9616147d..6cfc8e3b 100644 --- a/extension.neon +++ b/extension.neon @@ -48,6 +48,12 @@ services: tags: - phpstan.rules.rule + # ShouldHaveOnlyOnePublicMethod rules + - + class: PHPat\Rule\Assertion\Declaration\ShouldHaveOnlyOnePublicMethod\HasOnlyOnePublicMethodRule + tags: + - phpstan.rules.rule + # # # # # RELATION RULES # # # # # # ShouldImplement rules diff --git a/src/Rule/Assertion/Declaration/ShouldHaveOnlyOnePublicMethod/HasOnlyOnePublicMethodRule.php b/src/Rule/Assertion/Declaration/ShouldHaveOnlyOnePublicMethod/HasOnlyOnePublicMethodRule.php new file mode 100644 index 00000000..f173636b --- /dev/null +++ b/src/Rule/Assertion/Declaration/ShouldHaveOnlyOnePublicMethod/HasOnlyOnePublicMethodRule.php @@ -0,0 +1,10 @@ +applyShould($ruleName, $subject, $meetsDeclaration, $tips); + } + + protected function getMessage(string $ruleName, string $subject): string + { + return $this->prepareMessage($ruleName, sprintf('%s should have only one public method', $subject)); + } +} diff --git a/src/Rule/Extractor/Declaration/OnePublicMethodExtractor.php b/src/Rule/Extractor/Declaration/OnePublicMethodExtractor.php new file mode 100644 index 00000000..34aa0c83 --- /dev/null +++ b/src/Rule/Extractor/Declaration/OnePublicMethodExtractor.php @@ -0,0 +1,29 @@ +getClassReflection()->getName()); + $methods = $reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC); + + return count($methods) === 1; + } +} diff --git a/src/Test/Builder/AssertionStep.php b/src/Test/Builder/AssertionStep.php index 330c0779..a84cdbfe 100644 --- a/src/Test/Builder/AssertionStep.php +++ b/src/Test/Builder/AssertionStep.php @@ -5,6 +5,7 @@ use PHPat\Rule\Assertion\Declaration\ShouldBeAbstract\ShouldBeAbstract; use PHPat\Rule\Assertion\Declaration\ShouldBeFinal\ShouldBeFinal; use PHPat\Rule\Assertion\Declaration\ShouldBeReadonly\ShouldBeReadonly; +use PHPat\Rule\Assertion\Declaration\ShouldHaveOnlyOnePublicMethod\ShouldHaveOnlyOnePublicMethod; use PHPat\Rule\Assertion\Declaration\ShouldNotBeAbstract\ShouldNotBeAbstract; use PHPat\Rule\Assertion\Declaration\ShouldNotBeFinal\ShouldNotBeFinal; use PHPat\Rule\Assertion\Relation\CanOnlyDepend\CanOnlyDepend; @@ -100,4 +101,11 @@ public function shouldExtend(): TargetStep return new TargetStep($this->rule); } + + public function shouldHaveOnlyOnePublicMethod(): Rule + { + $this->rule->assertion = ShouldHaveOnlyOnePublicMethod::class; + + return new BuildStep($this->rule); + } } diff --git a/tests/fixtures/Special/ClassWithOnePublicMethod.php b/tests/fixtures/Special/ClassWithOnePublicMethod.php new file mode 100644 index 00000000..009cea99 --- /dev/null +++ b/tests/fixtures/Special/ClassWithOnePublicMethod.php @@ -0,0 +1,13 @@ + + * @internal + * @coversNothing + */ +class ClassWithOnlyOnePublicMethodTest extends RuleTestCase +{ + public const RULE_NAME = 'test_FixtureClassShouldHaveOnlyOnePublicMethod'; + + public function testRule(): void + { + $this->analyse(['tests/fixtures/FixtureClass.php'], [ + [sprintf('%s should have only one public method', FixtureClass::class), 29], + ]); + } + + protected function getRule(): Rule + { + $testParser = FakeTestParser::create( + self::RULE_NAME, + ShouldHaveOnlyOnePublicMethod::class, + [new Classname(FixtureClass::class, false)], + [] + ); + + return new HasOnlyOnePublicMethodRule( + new StatementBuilderFactory($testParser), + new Configuration(false, true, false), + $this->createReflectionProvider(), + self::getContainer()->getByType(FileTypeMapper::class) + ); + } + +} diff --git a/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/GoodImplementationClassWithOnlyOnePublicMethodTest.php b/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/GoodImplementationClassWithOnlyOnePublicMethodTest.php new file mode 100644 index 00000000..98d44655 --- /dev/null +++ b/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/GoodImplementationClassWithOnlyOnePublicMethodTest.php @@ -0,0 +1,48 @@ + + * @internal + * @coversNothing + */ +class GoodImplementationClassWithOnlyOnePublicMethodTest extends RuleTestCase +{ + public const RULE_NAME = 'test_FixtureClassShouldHaveOnlyOnePublicMethod'; + + public function testRule(): void + { + $this->analyse(['tests/fixtures/Special/ClassWithOnePublicMethod.php'], []); + } + + protected function getRule(): Rule + { + $testParser = FakeTestParser::create( + self::RULE_NAME, + ShouldHaveOnlyOnePublicMethod::class, + [new Classname(ClassWithOnePublicMethod::class, false)], + [] + ); + + return new HasOnlyOnePublicMethodRule( + new StatementBuilderFactory($testParser), + new Configuration(false, true, false), + $this->createReflectionProvider(), + self::getContainer()->getByType(FileTypeMapper::class) + ); + } + +} From cdf07f61ab3a3eb86e46ea4a1ca8f00e570f6ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Tue, 3 Oct 2023 16:40:56 +0200 Subject: [PATCH 2/3] feat: add ShouldHaveOnlyOnePublicMethod rule --- .../HasOnlyOnePublicMethodRule.php | 4 ++-- .../ShouldHaveOnlyOnePublicMethod.php | 2 +- .../Declaration/OnePublicMethodExtractor.php | 14 ++++++++------ .../fixtures/Special/ClassWithOnePublicMethod.php | 9 ++++++++- .../ClassWithOnlyOnePublicMethodTest.php | 3 +-- ...lementationClassWithOnlyOnePublicMethodTest.php | 4 +--- 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/Rule/Assertion/Declaration/ShouldHaveOnlyOnePublicMethod/HasOnlyOnePublicMethodRule.php b/src/Rule/Assertion/Declaration/ShouldHaveOnlyOnePublicMethod/HasOnlyOnePublicMethodRule.php index f173636b..2da78bdb 100644 --- a/src/Rule/Assertion/Declaration/ShouldHaveOnlyOnePublicMethod/HasOnlyOnePublicMethodRule.php +++ b/src/Rule/Assertion/Declaration/ShouldHaveOnlyOnePublicMethod/HasOnlyOnePublicMethodRule.php @@ -1,10 +1,10 @@ -getClassReflection()->getName()); - $methods = $reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC); + $reflectionClass = $node->getClassReflection()->getNativeReflection(); + $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); - return count($methods) === 1; + $methodsWithoutConstructor = array_filter( + $methods, + fn (\ReflectionMethod $method) => $method->getName() !== '__construct' + ); + + return count($methodsWithoutConstructor) === 1; } } diff --git a/tests/fixtures/Special/ClassWithOnePublicMethod.php b/tests/fixtures/Special/ClassWithOnePublicMethod.php index 009cea99..2614aa75 100644 --- a/tests/fixtures/Special/ClassWithOnePublicMethod.php +++ b/tests/fixtures/Special/ClassWithOnePublicMethod.php @@ -6,8 +6,15 @@ class ClassWithOnePublicMethod { public const CONSTANT = 'constant'; public string $property = 'property'; + public string $anotherProperty; - public function publicMethod(): bool { + public function __construct() + { + $this->anotherProperty = 'anotherProperty'; + } + + public function publicMethod(): bool + { return true; } } diff --git a/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/ClassWithOnlyOnePublicMethodTest.php b/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/ClassWithOnlyOnePublicMethodTest.php index 663e419b..72b9c67e 100644 --- a/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/ClassWithOnlyOnePublicMethodTest.php +++ b/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/ClassWithOnlyOnePublicMethodTest.php @@ -1,4 +1,4 @@ -getByType(FileTypeMapper::class) ); } - } diff --git a/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/GoodImplementationClassWithOnlyOnePublicMethodTest.php b/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/GoodImplementationClassWithOnlyOnePublicMethodTest.php index 98d44655..dd784e7f 100644 --- a/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/GoodImplementationClassWithOnlyOnePublicMethodTest.php +++ b/tests/unit/rules/ShouldHaveOnlyOnePublicMethod/GoodImplementationClassWithOnlyOnePublicMethodTest.php @@ -1,4 +1,4 @@ -getByType(FileTypeMapper::class) ); } - } From 5d530db9735aa52ca702db9e6d91493b1e06c990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Tue, 3 Oct 2023 16:53:59 +0200 Subject: [PATCH 3/3] fix: stop depending on ReflectionMethod --- src/Rule/Extractor/Declaration/OnePublicMethodExtractor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rule/Extractor/Declaration/OnePublicMethodExtractor.php b/src/Rule/Extractor/Declaration/OnePublicMethodExtractor.php index 3627854f..f854267c 100644 --- a/src/Rule/Extractor/Declaration/OnePublicMethodExtractor.php +++ b/src/Rule/Extractor/Declaration/OnePublicMethodExtractor.php @@ -19,11 +19,11 @@ public function getNodeType(): string protected function meetsDeclaration(Node $node, Scope $scope): bool { $reflectionClass = $node->getClassReflection()->getNativeReflection(); - $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); + $methods = $reflectionClass->getMethods(1); $methodsWithoutConstructor = array_filter( $methods, - fn (\ReflectionMethod $method) => $method->getName() !== '__construct' + fn ($method) => $method->getName() !== '__construct' ); return count($methodsWithoutConstructor) === 1;