From 96d24214755c07dc5e7d7595f4233b080412ed04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sun, 10 Dec 2017 22:36:27 +0100 Subject: [PATCH 001/277] Add missing methods that generate mocks - `createPartialMock()` is available since PHPUnit 5.5.0 - `createTestProxy()` is available since PHPUnit 6.0.0 --- src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php b/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php index 68038e04..6d74e3f9 100644 --- a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php @@ -16,6 +16,8 @@ class CreateMockDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMetho private $methods = [ 'createMock' => 0, 'createConfiguredMock' => 0, + 'createPartialMock' => 0, + 'createTestProxy' => 0, 'getMockForAbstractClass' => 0, 'getMockFromWsdl' => 1, ]; From 6501e8e484f15f79c5a22abb50912f3097ee4b6b Mon Sep 17 00:00:00 2001 From: Martin Hujer Date: Mon, 11 Dec 2017 16:11:13 +0100 Subject: [PATCH 002/277] add AssertSameDifferentTypesRuleTest --- phpstan.neon | 4 ++ .../AssertSameDifferentTypesRuleTest.php | 45 ++++++++++++++++ tests/Rules/PHPUnit/data/assert-same.php | 51 +++++++++++++++++++ tests/bootstrap.php | 4 ++ 4 files changed, 104 insertions(+) create mode 100644 tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/assert-same.php diff --git a/phpstan.neon b/phpstan.neon index 5ec89139..7e41decb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,3 +2,7 @@ includes: - extension.neon - rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon + +parameters: + excludes_analyse: + - */tests/*/data/* diff --git a/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php new file mode 100644 index 00000000..ac66d1c0 --- /dev/null +++ b/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php @@ -0,0 +1,45 @@ +analyse([__DIR__ . '/data/assert-same.php'], [ + [ + 'Call to assertSame() with different types string and int will always result in test failure.', + 10, + ], + [ + 'Call to assertSame() with different types string and stdClass will always result in test failure.', + 11, + ], + [ + 'Call to assertSame() with different types int and string will always result in test failure.', + 12, + ], + [ + 'Call to assertSame() with different types string and int will always result in test failure.', + 13, + ], + [ + 'Call to assertSame() with different types array and array will always result in test failure.', + 14, + ], + [ + 'Call to assertSame() with different types array and array will always result in test failure.', + 35, + ], + ]); + } + +} diff --git a/tests/Rules/PHPUnit/data/assert-same.php b/tests/Rules/PHPUnit/data/assert-same.php new file mode 100644 index 00000000..8d3a18c6 --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-same.php @@ -0,0 +1,51 @@ +assertSame('1', 1); + $this->assertSame('1', new \stdClass()); + $this->assertSame(1, $this->returnsString()); + $this->assertSame('1', self::returnsInt()); + $this->assertSame(['a', 'b'], [1, 2]); + } + + private function returnsString(): string + { + return 'foo'; + } + + private static function returnsInt(): int + { + return 1; + } + + public function testArrays() + { + /** @var string[] $a */ + $a = ['x']; + + /** @var int[] $b */ + $b = [1, 2]; + + $this->assertSame($a, $b); + } + + public function testLogicallyCorrectAssertSame() + { + $this->assertSame(1, 1); + $this->assertSame(['a'], ['a', 'b']); + $this->assertSame('1', '1'); + $this->assertSame('1', '2'); + $this->assertSame(new \stdClass(), new \stdClass()); + $this->assertSame('1', $this->returnsString()); + $this->assertSame(1, self::returnsInt()); + $this->assertSame(['a'], ['a', 1]); + $this->assertSame(['a', 2, 3.0], ['a', 1]); + } + +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 22e480dd..9dfa9f56 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,3 +1,7 @@ Date: Thu, 21 Dec 2017 15:01:30 +0100 Subject: [PATCH 003/277] improve AssertSameDifferentTypesRule test coverage --- .../AssertSameDifferentTypesRuleTest.php | 14 +++++++++++++- tests/Rules/PHPUnit/data/Foo.php | 13 +++++++++++++ tests/Rules/PHPUnit/data/assert-same.php | 17 +++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/Rules/PHPUnit/data/Foo.php diff --git a/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php index ac66d1c0..1a925b0d 100644 --- a/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php @@ -35,9 +35,21 @@ public function testRule() 'Call to assertSame() with different types array and array will always result in test failure.', 14, ], + [ + 'Call to assertSame() with different types string and int will always result in test failure.', + 16, + ], + [ + 'Call to assertSame() with different types string and int will always result in test failure.', + 17, + ], + [ + 'Call to assertSame() with different types string and int will always result in test failure.', + 18, + ], [ 'Call to assertSame() with different types array and array will always result in test failure.', - 35, + 39, ], ]); } diff --git a/tests/Rules/PHPUnit/data/Foo.php b/tests/Rules/PHPUnit/data/Foo.php new file mode 100644 index 00000000..c5248d77 --- /dev/null +++ b/tests/Rules/PHPUnit/data/Foo.php @@ -0,0 +1,13 @@ +assertSame(1, $this->returnsString()); $this->assertSame('1', self::returnsInt()); $this->assertSame(['a', 'b'], [1, 2]); + + self::assertSame('1', 2); // test self + static::assertSame('1', 2); // test static + parent::assertSame('1', 2); // test parent } private function returnsString(): string @@ -46,6 +50,19 @@ public function testLogicallyCorrectAssertSame() $this->assertSame(1, self::returnsInt()); $this->assertSame(['a'], ['a', 1]); $this->assertSame(['a', 2, 3.0], ['a', 1]); + self::assertSame(1, 2); // test self + static::assertSame(1, 2); // test static + parent::assertSame(1, 2); // test parent + } + + public function testOther() + { + // assertEquals is not checked + $this->assertEquals('1', 1); + + // only calls on \PHPUnit\Framework\TestCase are analyzed + $foo = new \Dummy\Foo(); + $foo->assertSame(); } } From 8b8a62247cf3c404ff6f4b5bd91070fc187c4c54 Mon Sep 17 00:00:00 2001 From: Martin Hujer Date: Thu, 21 Dec 2017 16:11:07 +0100 Subject: [PATCH 004/277] Move PHPCS exclusion pattern to phpcs.xml Otherwise it does not play nice with inline validation in PHPStorm --- build.xml | 2 -- phpcs.xml | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build.xml b/build.xml index 8fc91d1d..7b50b9c9 100644 --- a/build.xml +++ b/build.xml @@ -42,7 +42,6 @@ - @@ -59,7 +58,6 @@ - diff --git a/phpcs.xml b/phpcs.xml index 28ee4e94..2e093737 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -36,4 +36,5 @@ + tests/*/data From 967356e30e10e5b11ab9dc1786c9228f7ed8130a Mon Sep 17 00:00:00 2001 From: Martin Hujer Date: Thu, 21 Dec 2017 16:17:36 +0100 Subject: [PATCH 005/277] extract PHPUnit method call detection to helper --- src/Rules/PHPUnit/AssertRuleHelper.php | 52 +++++++++++++++++++ .../PHPUnit/AssertSameDifferentTypesRule.php | 29 +---------- 2 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 src/Rules/PHPUnit/AssertRuleHelper.php diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php new file mode 100644 index 00000000..6e2e8649 --- /dev/null +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -0,0 +1,52 @@ +getType($node->var); + } elseif ($node instanceof Node\Expr\StaticCall) { + if ($node->class instanceof Node\Name) { + $class = (string) $node->class; + if (in_array( + strtolower($class), + [ + 'self', + 'static', + 'parent', + ], + true + )) { + $calledOnType = new ObjectType($scope->getClassReflection()->getName()); + } else { + $calledOnType = new ObjectType($class); + } + } else { + $calledOnType = $scope->getType($node->class); + } + } else { + return false; + } + + if (!$testCaseType->isSuperTypeOf($calledOnType)->yes()) { + return false; + } + + return true; + } + +} diff --git a/src/Rules/PHPUnit/AssertSameDifferentTypesRule.php b/src/Rules/PHPUnit/AssertSameDifferentTypesRule.php index c16c6bcd..84d130e1 100644 --- a/src/Rules/PHPUnit/AssertSameDifferentTypesRule.php +++ b/src/Rules/PHPUnit/AssertSameDifferentTypesRule.php @@ -4,7 +4,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Type\ObjectType; class AssertSameDifferentTypesRule implements \PHPStan\Rules\Rule { @@ -21,33 +20,7 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - $testCaseType = new ObjectType(\PHPUnit\Framework\TestCase::class); - if ($node instanceof Node\Expr\MethodCall) { - $calledOnType = $scope->getType($node->var); - } elseif ($node instanceof Node\Expr\StaticCall) { - if ($node->class instanceof Node\Name) { - $class = (string) $node->class; - if (in_array( - strtolower($class), - [ - 'self', - 'static', - 'parent', - ], - true - )) { - $calledOnType = new ObjectType($scope->getClassReflection()->getName()); - } else { - $calledOnType = new ObjectType($class); - } - } else { - $calledOnType = $scope->getType($node->class); - } - } else { - return []; - } - - if (!$testCaseType->isSuperTypeOf($calledOnType)->yes()) { + if (!AssertRuleHelper::isMethodOrStaticCallOnTestCase($node, $scope)) { return []; } From 88d4efc5cfcac7994ac58b4c9908c8735137a9ab Mon Sep 17 00:00:00 2001 From: Martin Hujer Date: Thu, 21 Dec 2017 16:17:46 +0100 Subject: [PATCH 006/277] AssertSameNullExpectedRule --- README.md | 10 ++++ .../PHPUnit/AssertSameNullExpectedRule.php | 46 +++++++++++++++++++ strictRules.neon | 5 ++ .../AssertSameNullExpectedRuleTest.php | 29 ++++++++++++ .../data/assert-same-null-expected.php | 22 +++++++++ 5 files changed, 112 insertions(+) create mode 100644 src/Rules/PHPUnit/AssertSameNullExpectedRule.php create mode 100644 strictRules.neon create mode 100644 tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/assert-same-null-expected.php diff --git a/README.md b/README.md index 75b6fe92..1789e1fb 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ It also contains this framework-specific rule (can be enabled separately): * Check that both values passed to `assertSame()` method are of the same type. +It also contains this strict framework-specific rule (can be enabled separately): + +* Check that you are not using `assertSame()` with `null` as expected value. `assertNull()` should be used instead. + ## How to document mock objects in phpDocs? If you need to configure the mock even after you assign it to a property or return it from a method, you should add `PHPUnit_Framework_MockObject_MockObject` to the phpDoc: @@ -81,3 +85,9 @@ To perform framework-specific checks, include also this file: ``` - vendor/phpstan/phpstan-phpunit/rules.neon ``` + +To perform addition strict PHPUnit checks, include also this file: + +``` + - vendor/phpstan/phpstan-phpunit/strictRules.neon +``` diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php new file mode 100644 index 00000000..318c9479 --- /dev/null +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -0,0 +1,46 @@ +args) < 2) { + return []; + } + if (!is_string($node->name) || strtolower($node->name) !== 'assertsame') { + return []; + } + + $leftType = $scope->getType($node->args[0]->value); + + if ($leftType instanceof NullType) { + return [ + 'You should use assertNull() instead of assertSame(null, $actual).', + ]; + } + + return []; + } + +} diff --git a/strictRules.neon b/strictRules.neon new file mode 100644 index 00000000..bfeb46fc --- /dev/null +++ b/strictRules.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule + tags: + - phpstan.rules.rule diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php new file mode 100644 index 00000000..a56d8c77 --- /dev/null +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -0,0 +1,29 @@ +analyse([__DIR__ . '/data/assert-same-null-expected.php'], [ + [ + 'You should use assertNull() instead of assertSame(null, $actual).', + 10, + ], + [ + 'You should use assertNull() instead of assertSame(null, $actual).', + 13, + ], + ]); + } + +} diff --git a/tests/Rules/PHPUnit/data/assert-same-null-expected.php b/tests/Rules/PHPUnit/data/assert-same-null-expected.php new file mode 100644 index 00000000..e4cec93e --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-same-null-expected.php @@ -0,0 +1,22 @@ +assertSame(null, 'a'); + + $a = null; + $this->assertSame($a, 'b'); + + $this->assertSame('a', 'b'); // OK + + /** @var string|null $c */ + $c = null; + $this->assertSame($c, 'foo'); // nullable is OK + } + +} From f9c16dd1324694ae6c8e9bf91e30455cbc630798 Mon Sep 17 00:00:00 2001 From: Martin Hujer Date: Thu, 21 Dec 2017 16:37:08 +0100 Subject: [PATCH 007/277] AssertSameBooleanExpectedRule --- README.md | 4 +- .../PHPUnit/AssertSameBooleanExpectedRule.php | 53 +++++++++++++++++++ strictRules.neon | 4 ++ .../AssertSameBooleanExpectedRuleTest.php | 37 +++++++++++++ .../data/assert-same-boolean-expected.php | 24 +++++++++ 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php create mode 100644 tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/assert-same-boolean-expected.php diff --git a/README.md b/README.md index 1789e1fb..4ad440b3 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,10 @@ It also contains this framework-specific rule (can be enabled separately): * Check that both values passed to `assertSame()` method are of the same type. -It also contains this strict framework-specific rule (can be enabled separately): +It also contains this strict framework-specific rules (can be enabled separately): +* Check that you are not using `assertSame()` with `true` as expected value. `assertTrue()` should be used instead. +* Check that you are not using `assertSame()` with `false` as expected value. `assertFalse()` should be used instead. * Check that you are not using `assertSame()` with `null` as expected value. `assertNull()` should be used instead. ## How to document mock objects in phpDocs? diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php new file mode 100644 index 00000000..715ec965 --- /dev/null +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -0,0 +1,53 @@ +args) < 2) { + return []; + } + if (!is_string($node->name) || strtolower($node->name) !== 'assertsame') { + return []; + } + + $leftType = $scope->getType($node->args[0]->value); + + if ($leftType instanceof TrueBooleanType) { + return [ + 'You should use assertTrue() instead of assertSame() when expecting "true"', + ]; + } + + if ($leftType instanceof FalseBooleanType) { + return [ + 'You should use assertFalse() instead of assertSame() when expecting "false"', + ]; + } + + return []; + } + +} diff --git a/strictRules.neon b/strictRules.neon index bfeb46fc..98120ffb 100644 --- a/strictRules.neon +++ b/strictRules.neon @@ -1,4 +1,8 @@ services: + - + class: PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule + tags: + - phpstan.rules.rule - class: PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule tags: diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php new file mode 100644 index 00000000..a745ffc2 --- /dev/null +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -0,0 +1,37 @@ +analyse([__DIR__ . '/data/assert-same-boolean-expected.php'], [ + [ + 'You should use assertTrue() instead of assertSame() when expecting "true"', + 10, + ], + [ + 'You should use assertFalse() instead of assertSame() when expecting "false"', + 11, + ], + [ + 'You should use assertTrue() instead of assertSame() when expecting "true"', + 14, + ], + [ + 'You should use assertFalse() instead of assertSame() when expecting "false"', + 17, + ], + ]); + } + +} diff --git a/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php new file mode 100644 index 00000000..22d913e6 --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php @@ -0,0 +1,24 @@ +assertSame(true, 'a'); + $this->assertSame(false, 'a'); + + $truish = true; + $this->assertSame($truish, true); + + $falsish = false; + $this->assertSame($falsish, false); + + /** @var bool $a */ + $a = null; + $this->assertSame($a, 'b'); // OK + } + +} From 669990e486c1efe7d4a2f6d337c229f6a317795b Mon Sep 17 00:00:00 2001 From: Martin Hujer Date: Thu, 21 Dec 2017 16:45:47 +0100 Subject: [PATCH 008/277] AssertSameWithCountRule --- README.md | 1 + src/Rules/PHPUnit/AssertSameWithCountRule.php | 49 +++++++++++++++++++ strictRules.neon | 4 ++ .../PHPUnit/AssertSameWithCountRuleTest.php | 25 ++++++++++ .../Rules/PHPUnit/data/assert-same-count.php | 18 +++++++ 5 files changed, 97 insertions(+) create mode 100644 src/Rules/PHPUnit/AssertSameWithCountRule.php create mode 100644 tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/assert-same-count.php diff --git a/README.md b/README.md index 4ad440b3..e73c9012 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ It also contains this strict framework-specific rules (can be enabled separately * Check that you are not using `assertSame()` with `true` as expected value. `assertTrue()` should be used instead. * Check that you are not using `assertSame()` with `false` as expected value. `assertFalse()` should be used instead. * Check that you are not using `assertSame()` with `null` as expected value. `assertNull()` should be used instead. +* Check that you are not using `assertSame()` with `count($variable)` as second parameter. `assertCount($variable)` should be used instead. ## How to document mock objects in phpDocs? diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php new file mode 100644 index 00000000..d90612e6 --- /dev/null +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -0,0 +1,49 @@ +args) < 2) { + return []; + } + if (!is_string($node->name) || strtolower($node->name) !== 'assertsame') { + return []; + } + + $right = $node->args[1]->value; + + if ( + $right instanceof Node\Expr\FuncCall + && $right->name instanceof Node\Name + && strtolower($right->name->toString()) === 'count' + ) { + return [ + 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', + ]; + } + + return []; + } + +} diff --git a/strictRules.neon b/strictRules.neon index 98120ffb..db0671e0 100644 --- a/strictRules.neon +++ b/strictRules.neon @@ -7,3 +7,7 @@ services: class: PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule tags: - phpstan.rules.rule + - + class: PHPStan\Rules\PHPUnit\AssertSameWithCountRule + tags: + - phpstan.rules.rule diff --git a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php new file mode 100644 index 00000000..8171bcd7 --- /dev/null +++ b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php @@ -0,0 +1,25 @@ +analyse([__DIR__ . '/data/assert-same-count.php'], [ + [ + 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', + 10, + ], + ]); + } + +} diff --git a/tests/Rules/PHPUnit/data/assert-same-count.php b/tests/Rules/PHPUnit/data/assert-same-count.php new file mode 100644 index 00000000..0716ec7c --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-same-count.php @@ -0,0 +1,18 @@ +assertSame(5, count([1, 2, 3])); + } + + public function testAssertSameWithCountMethodIsOK() + { + $this->assertSame(5, $this->count()); // OK + } + +} From 81d5cc2506b1588edc040a7912d79e6f57b15cb5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 28 Dec 2017 16:23:12 +0100 Subject: [PATCH 009/277] Open 0.10-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 32ec0a92..6ce86e8b 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-master": "0.9-dev" + "dev-master": "0.10-dev" } }, "require": { From 1187ed57ef9e6e7ca1c8caf5c142ee3eaa0f6db5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 28 Dec 2017 16:23:31 +0100 Subject: [PATCH 010/277] Drop PHP 7.0 support --- .travis.yml | 1 - composer.json | 2 +- phpcs.xml | 4 ---- src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php | 2 +- tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php | 2 +- tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php | 2 +- tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php | 2 +- tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php | 2 +- 8 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4595318a..5edc6097 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: php php: - - 7.0 - 7.1 - 7.2 before_script: diff --git a/composer.json b/composer.json index 6ce86e8b..5db9663b 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } }, "require": { - "php": "~7.0", + "php": "~7.1", "phpstan/phpstan": "^0.9.1", "phpunit/phpunit": "^6.3" }, diff --git a/phpcs.xml b/phpcs.xml index 2e093737..8d1291b3 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -4,7 +4,6 @@ - @@ -14,7 +13,6 @@ - @@ -32,8 +30,6 @@ @dataProvider, @requires "/> - - tests/*/data diff --git a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php index 4c6afff6..202694d8 100644 --- a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php @@ -19,7 +19,7 @@ class MockBuilderDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMeth */ private $broker; - public function setBroker(Broker $broker) + public function setBroker(Broker $broker): void { $this->broker = $broker; } diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php index a745ffc2..63c608ec 100644 --- a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -12,7 +12,7 @@ protected function getRule(): Rule return new AssertSameBooleanExpectedRule(); } - public function testRule() + public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same-boolean-expected.php'], [ [ diff --git a/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php index 1a925b0d..4e81480e 100644 --- a/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php @@ -12,7 +12,7 @@ protected function getRule(): Rule return new AssertSameDifferentTypesRule(); } - public function testRule() + public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same.php'], [ [ diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php index a56d8c77..53bcf8c0 100644 --- a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -12,7 +12,7 @@ protected function getRule(): Rule return new AssertSameNullExpectedRule(); } - public function testRule() + public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same-null-expected.php'], [ [ diff --git a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php index 8171bcd7..eee5e0fa 100644 --- a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php @@ -12,7 +12,7 @@ protected function getRule(): Rule return new AssertSameWithCountRule(); } - public function testRule() + public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same-count.php'], [ [ From ad83a908e65362e562e024b74f39585959798f6f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 28 Jan 2018 15:49:51 +0100 Subject: [PATCH 011/277] Open 0.10-dev --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 5db9663b..cdbeea99 100644 --- a/composer.json +++ b/composer.json @@ -11,14 +11,14 @@ }, "require": { "php": "~7.1", - "phpstan/phpstan": "^0.9.1", + "phpstan/phpstan": "^0.10", "phpunit/phpunit": "^6.3" }, "require-dev": { "consistence/coding-standard": "^2.0", "jakub-onderka/php-parallel-lint": "^0.9.2", "phing/phing": "^2.16.0", - "phpstan/phpstan-strict-rules": "^0.9", + "phpstan/phpstan-strict-rules": "^0.10", "satooshi/php-coveralls": "^1.0", "slevomat/coding-standard": "^3.3.0" }, From 852411f841a37aeca2fa5af0002b0272c485c9bf Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 2 Feb 2018 10:45:47 +0100 Subject: [PATCH 012/277] Support PHPUnit 7, build with lowest and highest dependencies --- .travis.yml | 6 +++++- composer.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4595318a..06fe7f05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,13 @@ php: - 7.0 - 7.1 - 7.2 +env: + - dependencies=lowest + - dependencies=highest before_script: - composer self-update - - composer install + - if [ "$dependencies" = "lowest" ]; then composer update --prefer-lowest --no-interaction; fi; + - if [ "$dependencies" = "highest" ]; then composer update --no-interaction; fi; script: - vendor/bin/phing after_script: diff --git a/composer.json b/composer.json index 32ec0a92..8d52e004 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "require": { "php": "~7.0", "phpstan/phpstan": "^0.9.1", - "phpunit/phpunit": "^6.3" + "phpunit/phpunit": "^6.3 || ~7.0" }, "require-dev": { "consistence/coding-standard": "^2.0", From b26d14dd8328157ea65f1a43c2d70a7ab984387d Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Fri, 2 Feb 2018 08:57:34 -0200 Subject: [PATCH 013/277] Require PHPUnit 7 as we minimum require PHP 7.1 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ed626e91..4b8be9a5 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "require": { "php": "~7.1", "phpstan/phpstan": "^0.10", - "phpunit/phpunit": "^6.3 || ~7.0" + "phpunit/phpunit": "^7.0" }, "require-dev": { "consistence/coding-standard": "^2.0", From f23194b719d7b46d49d7a39663639c5a5a309f0c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 4 Mar 2018 12:01:53 +0100 Subject: [PATCH 014/277] Rewritten rules.neon and strictRules.neon with RulesExtension --- rules.neon | 7 ++----- strictRules.neon | 17 ++++------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/rules.neon b/rules.neon index 161e3e85..b6d0af7f 100644 --- a/rules.neon +++ b/rules.neon @@ -1,5 +1,2 @@ -services: - - - class: PHPStan\Rules\PHPUnit\AssertSameDifferentTypesRule - tags: - - phpstan.rules.rule +rules: + - PHPStan\Rules\PHPUnit\AssertSameDifferentTypesRule diff --git a/strictRules.neon b/strictRules.neon index db0671e0..ec02f7d7 100644 --- a/strictRules.neon +++ b/strictRules.neon @@ -1,13 +1,4 @@ -services: - - - class: PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule - tags: - - phpstan.rules.rule - - - class: PHPStan\Rules\PHPUnit\AssertSameWithCountRule - tags: - - phpstan.rules.rule +rules: + - PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule + - PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule + - PHPStan\Rules\PHPUnit\AssertSameWithCountRule From a0cd25e0eaee987c03b4e064b5739c7d134d0784 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 4 Mar 2018 12:02:05 +0100 Subject: [PATCH 015/277] Updated for constant types --- .../PHPUnit/AssertSameBooleanExpectedRule.php | 18 ++++----- .../CreateMockDynamicReturnTypeExtension.php | 20 ++-------- ...tMockBuilderDynamicReturnTypeExtension.php | 19 ++------- .../AssertSameDifferentTypesRuleTest.php | 40 ++++++++++++++++--- 4 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 715ec965..67507b4b 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -4,8 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Type\FalseBooleanType; -use PHPStan\Type\TrueBooleanType; +use PHPStan\Type\Constant\ConstantBooleanType; class AssertSameBooleanExpectedRule implements \PHPStan\Rules\Rule { @@ -34,20 +33,19 @@ public function processNode(Node $node, Scope $scope): array } $leftType = $scope->getType($node->args[0]->value); - - if ($leftType instanceof TrueBooleanType) { - return [ - 'You should use assertTrue() instead of assertSame() when expecting "true"', - ]; + if (!$leftType instanceof ConstantBooleanType) { + return []; } - if ($leftType instanceof FalseBooleanType) { + if ($leftType->getValue()) { return [ - 'You should use assertFalse() instead of assertSame() when expecting "false"', + 'You should use assertTrue() instead of assertSame() when expecting "true"', ]; } - return []; + return [ + 'You should use assertFalse() instead of assertSame() when expecting "false"', + ]; } } diff --git a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php b/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php index 6d74e3f9..1014b37b 100644 --- a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -38,25 +39,12 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method if (!isset($methodCall->args[$argumentIndex])) { return $methodReflection->getReturnType(); } - $arg = $methodCall->args[$argumentIndex]->value; - if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { + $argType = $scope->getType($methodCall->args[$argumentIndex]->value); + if (!$argType instanceof ConstantStringType) { return $methodReflection->getReturnType(); } - $class = $arg->class; - if (!($class instanceof \PhpParser\Node\Name)) { - return $methodReflection->getReturnType(); - } - - $class = (string) $class; - - if ($class === 'static') { - return $methodReflection->getReturnType(); - } - - if ($class === 'self') { - $class = $scope->getClassReflection()->getName(); - } + $class = $argType->getValue(); return TypeCombinator::intersect( new ObjectType($class), diff --git a/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php index cf35ceb1..78568b11 100644 --- a/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; @@ -27,24 +28,12 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method if (count($methodCall->args) === 0) { return $mockBuilderType; } - $arg = $methodCall->args[0]->value; - if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { + $argType = $scope->getType($methodCall->args[0]->value); + if (!$argType instanceof ConstantStringType) { return $mockBuilderType; } - $class = $arg->class; - if (!($class instanceof \PhpParser\Node\Name)) { - return $mockBuilderType; - } - - $class = (string) $class; - if ($class === 'static') { - return $mockBuilderType; - } - - if ($class === 'self') { - $class = $scope->getClassReflection()->getName(); - } + $class = $argType->getValue(); if (!$mockBuilderType instanceof TypeWithClassName) { throw new \PHPStan\ShouldNotHappenException(); diff --git a/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php index 4e81480e..fe6204f4 100644 --- a/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php @@ -16,7 +16,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same.php'], [ [ - 'Call to assertSame() with different types string and int will always result in test failure.', + 'Call to assertSame() with different types string and int(1) will always result in test failure.', 10, ], [ @@ -24,7 +24,7 @@ public function testRule(): void 11, ], [ - 'Call to assertSame() with different types int and string will always result in test failure.', + 'Call to assertSame() with different types int(1) and string will always result in test failure.', 12, ], [ @@ -32,25 +32,53 @@ public function testRule(): void 13, ], [ - 'Call to assertSame() with different types array and array will always result in test failure.', + 'Call to assertSame() with different types array and array will always result in test failure.', 14, ], [ - 'Call to assertSame() with different types string and int will always result in test failure.', + 'Call to assertSame() with different types string and int(2) will always result in test failure.', 16, ], [ - 'Call to assertSame() with different types string and int will always result in test failure.', + 'Call to assertSame() with different types string and int(2) will always result in test failure.', 17, ], [ - 'Call to assertSame() with different types string and int will always result in test failure.', + 'Call to assertSame() with different types string and int(2) will always result in test failure.', 18, ], [ 'Call to assertSame() with different types array and array will always result in test failure.', 39, ], + [ + 'Call to assertSame() with different types array and array will always result in test failure.', + 45, + ], + [ + 'Call to assertSame() with different types string and string will always result in test failure.', + 47, + ], + [ + 'Call to assertSame() with different types array and array will always result in test failure.', + 51, + ], + [ + 'Call to assertSame() with different types array and array will always result in test failure.', + 52, + ], + [ + 'Call to assertSame() with different types int(1) and int(2) will always result in test failure.', + 53, + ], + [ + 'Call to assertSame() with different types int(1) and int(2) will always result in test failure.', + 54, + ], + [ + 'Call to assertSame() with different types int(1) and int(2) will always result in test failure.', + 55, + ], ]); } From 646b514ed6e9094d6c10b83a4ca3b3521732b6a3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 4 Mar 2018 12:02:49 +0100 Subject: [PATCH 016/277] Build - enable strictRules.neon --- phpstan.neon | 1 + 1 file changed, 1 insertion(+) diff --git a/phpstan.neon b/phpstan.neon index 7e41decb..a707ffd6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,7 @@ includes: - extension.neon - rules.neon + - strictRules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon parameters: From 3e538dd76564341ab0f7aa56f9cf78a068d17a23 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 2 Apr 2018 06:44:52 +0200 Subject: [PATCH 017/277] Update PHPStan --- phpstan.neon | 17 +++++++++++++++++ src/Rules/PHPUnit/AssertRuleHelper.php | 26 ++++++++++++-------------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index a707ffd6..98f7bd12 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,3 +7,20 @@ includes: parameters: excludes_analyse: - */tests/*/data/* + +services: + scopeIsInClass: + class: PHPStan\Build\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInClass + removeNullMethodName: getClassReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + + scopeIsInTrait: + class: PHPStan\Build\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInTrait + removeNullMethodName: getTraitReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index 6e2e8649..ae74086e 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -9,11 +9,6 @@ class AssertRuleHelper { - /** - * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node - * @param \PHPStan\Analyser\Scope $scope - * @return bool - */ public static function isMethodOrStaticCallOnTestCase(Node $node, Scope $scope): bool { $testCaseType = new ObjectType(\PHPUnit\Framework\TestCase::class); @@ -22,15 +17,18 @@ public static function isMethodOrStaticCallOnTestCase(Node $node, Scope $scope): } elseif ($node instanceof Node\Expr\StaticCall) { if ($node->class instanceof Node\Name) { $class = (string) $node->class; - if (in_array( - strtolower($class), - [ - 'self', - 'static', - 'parent', - ], - true - )) { + if ( + $scope->isInClass() + && in_array( + strtolower($class), + [ + 'self', + 'static', + 'parent', + ], + true + ) + ) { $calledOnType = new ObjectType($scope->getClassReflection()->getName()); } else { $calledOnType = new ObjectType($class); From 193c0fa2f5919500b8e8520892aac4fbe96c3d6f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 2 Apr 2018 07:04:14 +0200 Subject: [PATCH 018/277] Type-specifying extension for asserts (function and instance/static method calls) --- extension.neon | 12 + .../AssertFunctionTypeSpecifyingExtension.php | 52 +++++ .../AssertMethodTypeSpecifyingExtension.php | 57 +++++ ...ertStaticMethodTypeSpecifyingExtension.php | 57 +++++ .../AssertTypeSpecifyingExtensionHelper.php | 216 ++++++++++++++++++ 5 files changed, 394 insertions(+) create mode 100644 src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php create mode 100644 src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php create mode 100644 src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php create mode 100644 src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php diff --git a/extension.neon b/extension.neon index f3a1e35c..51c5c8c0 100644 --- a/extension.neon +++ b/extension.neon @@ -6,6 +6,18 @@ parameters: - markTestSkipped services: + - + class: PHPStan\Type\PHPUnit\Assert\AssertFunctionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: PHPStan\Type\PHPUnit\Assert\AssertMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + - + class: PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension - class: PHPStan\Type\PHPUnit\CreateMockDynamicReturnTypeExtension tags: diff --git a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php new file mode 100644 index 00000000..6a684d36 --- /dev/null +++ b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php @@ -0,0 +1,52 @@ +typeSpecifier = $typeSpecifier; + } + + public function isFunctionSupported( + FunctionReflection $functionReflection, + FuncCall $node, + TypeSpecifierContext $context + ): bool + { + return AssertTypeSpecifyingExtensionHelper::isSupported( + $functionReflection->getName(), + $node->args + ); + } + + public function specifyTypes( + FunctionReflection $functionReflection, + FuncCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return AssertTypeSpecifyingExtensionHelper::specifyTypes( + $this->typeSpecifier, + $scope, + $functionReflection->getName(), + $node->args + ); + } + +} diff --git a/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php new file mode 100644 index 00000000..310ebda2 --- /dev/null +++ b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php @@ -0,0 +1,57 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \PHPUnit\Framework\TestCase::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return AssertTypeSpecifyingExtensionHelper::isSupported( + $methodReflection->getName(), + $node->args + ); + } + + public function specifyTypes( + MethodReflection $functionReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return AssertTypeSpecifyingExtensionHelper::specifyTypes( + $this->typeSpecifier, + $scope, + $functionReflection->getName(), + $node->args + ); + } + +} diff --git a/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php new file mode 100644 index 00000000..9bcca873 --- /dev/null +++ b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php @@ -0,0 +1,57 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \PHPUnit\Framework\TestCase::class; + } + + public function isStaticMethodSupported( + MethodReflection $methodReflection, + StaticCall $node, + TypeSpecifierContext $context + ): bool + { + return AssertTypeSpecifyingExtensionHelper::isSupported( + $methodReflection->getName(), + $node->args + ); + } + + public function specifyTypes( + MethodReflection $functionReflection, + StaticCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return AssertTypeSpecifyingExtensionHelper::specifyTypes( + $this->typeSpecifier, + $scope, + $functionReflection->getName(), + $node->args + ); + } + +} diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php new file mode 100644 index 00000000..0f56c069 --- /dev/null +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -0,0 +1,216 @@ += (count($resolverReflection->getMethod('__invoke')->getParameters()) - 1); + } + + private static function trimName(string $name): string + { + if (substr($name, 0, strlen('assert')) !== 'assert') { + return $name; + } + + $name = substr($name, strlen('assert')); + if (substr($name, 0, 3) === 'Not') { + $name = substr($name, 3); + } + + return $name; + } + + /** + * @param TypeSpecifier $typeSpecifier + * @param Scope $scope + * @param string $name + * @param \PhpParser\Node\Arg[] $args $args + * @return SpecifiedTypes + */ + public static function specifyTypes( + TypeSpecifier $typeSpecifier, + Scope $scope, + string $name, + array $args + ): SpecifiedTypes + { + $expression = self::createExpression($scope, $name, $args); + if ($expression === null) { + return new SpecifiedTypes([], []); + } + return $typeSpecifier->specifyTypesInCondition( + $scope, + $expression, + TypeSpecifierContext::createTruthy() + ); + } + + /** + * @param Scope $scope + * @param string $name + * @param \PhpParser\Node\Arg[] $args + * @return \PhpParser\Node\Expr|null + */ + private static function createExpression( + Scope $scope, + string $name, + array $args + ): ?\PhpParser\Node\Expr + { + $trimmedName = self::trimName($name); + $resolvers = self::getExpressionResolvers(); + $resolver = $resolvers[$trimmedName]; + $expression = $resolver($scope, ...$args); + if ($expression === null) { + return null; + } + + if (strpos($name, 'Not') !== false) { + $expression = new \PhpParser\Node\Expr\BooleanNot($expression); + } + + return $expression; + } + + /** + * @return \Closure[] + */ + private static function getExpressionResolvers(): array + { + if (self::$resolvers === null) { + self::$resolvers = [ + 'InstanceOf' => function (Scope $scope, Arg $class, Arg $object) { + $classType = $scope->getType($class->value); + if (!$classType instanceof ConstantStringType) { + return null; + } + + return new \PhpParser\Node\Expr\Instanceof_( + $object->value, + new \PhpParser\Node\Name($classType->getValue()) + ); + }, + 'Same' => function (Scope $scope, Arg $expected, Arg $actual) { + return new \PhpParser\Node\Expr\BinaryOp\Identical( + $expected->value, + $actual->value + ); + }, + 'True' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\BinaryOp\Identical( + $actual->value, + new \PhpParser\Node\Expr\ConstFetch(new Name('true')) + ); + }, + 'False' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\BinaryOp\Identical( + $actual->value, + new \PhpParser\Node\Expr\ConstFetch(new Name('false')) + ); + }, + 'Null' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\BinaryOp\Identical( + $actual->value, + new \PhpParser\Node\Expr\ConstFetch(new Name('null')) + ); + }, + 'InternalType' => function (Scope $scope, Arg $type, Arg $value) { + $typeType = $scope->getType($type->value); + if (!$typeType instanceof ConstantStringType) { + return null; + } + + switch ($typeType->getValue()) { + case 'numeric': + $functionName = 'is_numeric'; + break; + case 'integer': + case 'int': + $functionName = 'is_int'; + break; + + case 'double': + case 'float': + case 'real': + $functionName = 'is_float'; + break; + + case 'string': + $functionName = 'is_string'; + break; + + case 'boolean': + case 'bool': + $functionName = 'is_bool'; + break; + + case 'null': + $functionName = 'is_null'; + break; + + case 'array': + $functionName = 'is_array'; + break; + + case 'object': + $functionName = 'is_object'; + break; + + case 'resource': + $functionName = 'is_resource'; + break; + + case 'callable': + $functionName = 'is_callable'; + break; + default: + return null; + } + + return new \PhpParser\Node\Expr\FuncCall( + new Name($functionName), + [ + $value, + ] + ); + }, + ]; + } + + return self::$resolvers; + } + +} From a4b4ff36179d4589ba37860a09c0643d834cc75b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 2 Apr 2018 11:08:46 +0200 Subject: [PATCH 019/277] Support for assertInternalType('is_scalar', ...) --- .../PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 0f56c069..507cb252 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -177,6 +177,10 @@ private static function getExpressionResolvers(): array $functionName = 'is_bool'; break; + case 'scalar': + $functionName = 'is_scalar'; + break; + case 'null': $functionName = 'is_null'; break; From c7aef3136ff4cea738411f13376ef6f9e07bf8fc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 2 Apr 2018 11:22:12 +0200 Subject: [PATCH 020/277] AssertSameDifferentTypesRule is obsolete --- README.md | 12 +-- phpstan.neon | 1 - rules.neon | 4 +- .../PHPUnit/AssertSameDifferentTypesRule.php | 50 ----------- strictRules.neon | 4 - .../AssertSameDifferentTypesRuleTest.php | 85 ------------------- ...AssertSameMethodDifferentTypesRuleTest.php | 85 +++++++++++++++++++ ...SameStaticMethodDifferentTypesRuleTest.php | 57 +++++++++++++ 8 files changed, 147 insertions(+), 151 deletions(-) delete mode 100644 src/Rules/PHPUnit/AssertSameDifferentTypesRule.php delete mode 100644 strictRules.neon delete mode 100644 tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php create mode 100644 tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php create mode 100644 tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php diff --git a/README.md b/README.md index e73c9012..85fd7a4f 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,8 @@ This extension provides following features: * `getMock()` called on `MockBuilder` is also supported. * Interprets `Foo|PHPUnit_Framework_MockObject_MockObject` in phpDoc so that it results in an intersection type instead of a union type. * Defines early terminating method calls for the `PHPUnit\Framework\TestCase` class to prevent undefined variable errors. - -It also contains this framework-specific rule (can be enabled separately): - -* Check that both values passed to `assertSame()` method are of the same type. +* Specifies types of expressions passed to various `assert` methods like `assertInstanceOf`, `assertTrue`, `assertInternalType` etc. +* Combined with PHPStan's level 4, it points out always-true and always-false asserts like `assertTrue(true)` etc. It also contains this strict framework-specific rules (can be enabled separately): @@ -88,9 +86,3 @@ To perform framework-specific checks, include also this file: ``` - vendor/phpstan/phpstan-phpunit/rules.neon ``` - -To perform addition strict PHPUnit checks, include also this file: - -``` - - vendor/phpstan/phpstan-phpunit/strictRules.neon -``` diff --git a/phpstan.neon b/phpstan.neon index 98f7bd12..8468a2e5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,6 @@ includes: - extension.neon - rules.neon - - strictRules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon parameters: diff --git a/rules.neon b/rules.neon index b6d0af7f..ec02f7d7 100644 --- a/rules.neon +++ b/rules.neon @@ -1,2 +1,4 @@ rules: - - PHPStan\Rules\PHPUnit\AssertSameDifferentTypesRule + - PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule + - PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule + - PHPStan\Rules\PHPUnit\AssertSameWithCountRule diff --git a/src/Rules/PHPUnit/AssertSameDifferentTypesRule.php b/src/Rules/PHPUnit/AssertSameDifferentTypesRule.php deleted file mode 100644 index 84d130e1..00000000 --- a/src/Rules/PHPUnit/AssertSameDifferentTypesRule.php +++ /dev/null @@ -1,50 +0,0 @@ -args) < 2) { - return []; - } - if (!is_string($node->name) || strtolower($node->name) !== 'assertsame') { - return []; - } - - $leftType = $scope->getType($node->args[0]->value); - $rightType = $scope->getType($node->args[1]->value); - - if ($leftType->isSuperTypeOf($rightType)->no()) { - return [ - sprintf( - 'Call to assertSame() with different types %s and %s will always result in test failure.', - $leftType->describe(), - $rightType->describe() - ), - ]; - } - - return []; - } - -} diff --git a/strictRules.neon b/strictRules.neon deleted file mode 100644 index ec02f7d7..00000000 --- a/strictRules.neon +++ /dev/null @@ -1,4 +0,0 @@ -rules: - - PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule - - PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule - - PHPStan\Rules\PHPUnit\AssertSameWithCountRule diff --git a/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php deleted file mode 100644 index fe6204f4..00000000 --- a/tests/Rules/PHPUnit/AssertSameDifferentTypesRuleTest.php +++ /dev/null @@ -1,85 +0,0 @@ -analyse([__DIR__ . '/data/assert-same.php'], [ - [ - 'Call to assertSame() with different types string and int(1) will always result in test failure.', - 10, - ], - [ - 'Call to assertSame() with different types string and stdClass will always result in test failure.', - 11, - ], - [ - 'Call to assertSame() with different types int(1) and string will always result in test failure.', - 12, - ], - [ - 'Call to assertSame() with different types string and int will always result in test failure.', - 13, - ], - [ - 'Call to assertSame() with different types array and array will always result in test failure.', - 14, - ], - [ - 'Call to assertSame() with different types string and int(2) will always result in test failure.', - 16, - ], - [ - 'Call to assertSame() with different types string and int(2) will always result in test failure.', - 17, - ], - [ - 'Call to assertSame() with different types string and int(2) will always result in test failure.', - 18, - ], - [ - 'Call to assertSame() with different types array and array will always result in test failure.', - 39, - ], - [ - 'Call to assertSame() with different types array and array will always result in test failure.', - 45, - ], - [ - 'Call to assertSame() with different types string and string will always result in test failure.', - 47, - ], - [ - 'Call to assertSame() with different types array and array will always result in test failure.', - 51, - ], - [ - 'Call to assertSame() with different types array and array will always result in test failure.', - 52, - ], - [ - 'Call to assertSame() with different types int(1) and int(2) will always result in test failure.', - 53, - ], - [ - 'Call to assertSame() with different types int(1) and int(2) will always result in test failure.', - 54, - ], - [ - 'Call to assertSame() with different types int(1) and int(2) will always result in test failure.', - 55, - ], - ]); - } - -} diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php new file mode 100644 index 00000000..e3421799 --- /dev/null +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -0,0 +1,85 @@ +analyse([__DIR__ . '/data/assert-same.php'], [ + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 10, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 11, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 12, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 13, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 14, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 39, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to true.', + 44, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 45, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to true.', + 46, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 47, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to true.', + 48, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 51, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 52, + ], + ]); + } + +} diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php new file mode 100644 index 00000000..4fe62f4a --- /dev/null +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -0,0 +1,57 @@ +analyse([__DIR__ . '/data/assert-same.php'], [ + [ + 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 16, + ], + [ + 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 17, + ], + [ + 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 18, + ], + [ + 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 53, + ], + [ + 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 54, + ], + [ + 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 55, + ], + ]); + } + +} From 5115c2bfb80b22e8dd7ef9d0c64e8583b92c08f9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 2 Apr 2018 12:47:24 +0200 Subject: [PATCH 021/277] Updated type descriptions in test for detecting wrong asserts --- ...AssertSameMethodDifferentTypesRuleTest.php | 26 +++++++++---------- ...SameStaticMethodDifferentTypesRuleTest.php | 12 ++++----- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index e3421799..7691fdc9 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -28,55 +28,55 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same.php'], [ [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with string and int(1) will always evaluate to false.', 10, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with string and stdClass will always evaluate to false.', 11, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with int(1) and string will always evaluate to false.', 12, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with string and int will always evaluate to false.', 13, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', 14, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', 39, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to true.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with int(1) and int(1) will always evaluate to true.', 44, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', 45, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to true.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with string and string will always evaluate to true.', 46, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with string and string will always evaluate to false.', 47, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to true.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with stdClass and stdClass will always evaluate to true.', 48, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', 51, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', 52, ], ]); diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index 4fe62f4a..eacbba6d 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -28,27 +28,27 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same.php'], [ [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with string and int(2) will always evaluate to false.', 16, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with string and int(2) will always evaluate to false.', 17, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with string and int(2) will always evaluate to false.', 18, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with int(1) and int(2) will always evaluate to false.', 53, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with int(1) and int(2) will always evaluate to false.', 54, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with int(1) and int(2) will always evaluate to false.', 55, ], ]); From 9686cda7fc20fbb6dd31a208c96e4db249d86045 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 2 Apr 2018 16:04:30 +0200 Subject: [PATCH 022/277] Updated coding standard --- composer.json | 7 +++--- phpcs.xml | 24 ++++++++++++------- .../MockBuilderDynamicReturnTypeExtension.php | 4 +--- src/Type/PHPUnit/MockBuilderType.php | 4 +--- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 4b8be9a5..17b1a165 100644 --- a/composer.json +++ b/composer.json @@ -15,12 +15,13 @@ "phpunit/phpunit": "^7.0" }, "require-dev": { - "consistence/coding-standard": "^2.0", - "jakub-onderka/php-parallel-lint": "^0.9.2", + "consistence/coding-standard": "^3.0.1", + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "jakub-onderka/php-parallel-lint": "^1.0", "phing/phing": "^2.16.0", "phpstan/phpstan-strict-rules": "^0.10", "satooshi/php-coveralls": "^1.0", - "slevomat/coding-standard": "^3.3.0" + "slevomat/coding-standard": "^4.5.2" }, "autoload": { "psr-4": { diff --git a/phpcs.xml b/phpcs.xml index 8d1291b3..320dc350 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -2,17 +2,10 @@ - - - - + + - - - - - @@ -32,5 +25,18 @@ "/> + + + + + + + + + + + + + tests/*/data diff --git a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php index 202694d8..df622f0f 100644 --- a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php @@ -14,9 +14,7 @@ class MockBuilderDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension, \PHPStan\Reflection\BrokerAwareExtension { - /** - * @var \PHPStan\Broker\Broker - */ + /** @var \PHPStan\Broker\Broker */ private $broker; public function setBroker(Broker $broker): void diff --git a/src/Type/PHPUnit/MockBuilderType.php b/src/Type/PHPUnit/MockBuilderType.php index a76443e5..c1d76d08 100644 --- a/src/Type/PHPUnit/MockBuilderType.php +++ b/src/Type/PHPUnit/MockBuilderType.php @@ -7,9 +7,7 @@ class MockBuilderType extends \PHPStan\Type\ObjectType { - /** - * @var string - */ + /** @var string */ private $mockedClass; public function __construct( From 813ea2ef1b5d6dfec5b5565f41b58f385cd1ef3b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 13 Apr 2018 18:31:08 +0200 Subject: [PATCH 023/277] Updated PHPStan --- src/Type/PHPUnit/MockBuilderType.php | 5 +++-- ...AssertSameMethodDifferentTypesRuleTest.php | 22 +++++++++---------- ...SameStaticMethodDifferentTypesRuleTest.php | 12 +++++----- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Type/PHPUnit/MockBuilderType.php b/src/Type/PHPUnit/MockBuilderType.php index c1d76d08..e47f16b7 100644 --- a/src/Type/PHPUnit/MockBuilderType.php +++ b/src/Type/PHPUnit/MockBuilderType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\PHPUnit; use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\VerbosityLevel; class MockBuilderType extends \PHPStan\Type\ObjectType { @@ -24,9 +25,9 @@ public function getMockedClass(): string return $this->mockedClass; } - public function describe(): string + public function describe(VerbosityLevel $level): string { - return sprintf('%s<%s>', parent::describe(), $this->mockedClass); + return sprintf('%s<%s>', parent::describe($level), $this->mockedClass); } } diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 7691fdc9..be64d2b4 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -28,23 +28,23 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same.php'], [ [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with string and int(1) will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and 1 will always evaluate to false.', 10, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with string and stdClass will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and stdClass will always evaluate to false.', 11, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with int(1) and string will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with 1 and string will always evaluate to false.', 12, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with string and int will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and int will always evaluate to false.', 13, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\', \'b\') and array(1, 2) will always evaluate to false.', 14, ], [ @@ -52,19 +52,19 @@ public function testRule(): void 39, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with int(1) and int(1) will always evaluate to true.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with 1 and 1 will always evaluate to true.', 44, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\') and array(\'a\', \'b\') will always evaluate to false.', 45, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with string and string will always evaluate to true.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and \'1\' will always evaluate to true.', 46, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with string and string will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and \'2\' will always evaluate to false.', 47, ], [ @@ -72,11 +72,11 @@ public function testRule(): void 48, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\') and array(\'a\', 1) will always evaluate to false.', 51, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\', 2, 3.0) and array(\'a\', 1) will always evaluate to false.', 52, ], ]); diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index eacbba6d..61d395b2 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -28,27 +28,27 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same.php'], [ [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() with string and int(2) will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with \'1\' and 2 will always evaluate to false.', 16, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() with string and int(2) will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with \'1\' and 2 will always evaluate to false.', 17, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() with string and int(2) will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with \'1\' and 2 will always evaluate to false.', 18, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() with int(1) and int(2) will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with 1 and 2 will always evaluate to false.', 53, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() with int(1) and int(2) will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with 1 and 2 will always evaluate to false.', 54, ], [ - 'Call to static method PHPUnit\Framework\Assert::assertSame() with int(1) and int(2) will always evaluate to false.', + 'Call to static method PHPUnit\Framework\Assert::assertSame() with 1 and 2 will always evaluate to false.', 55, ], ]); From 7048cb46f55185d8d30e913c534e6a8e12e9f65c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 13 Apr 2018 18:32:33 +0200 Subject: [PATCH 024/277] Specify static calls on Assert too --- .../Assert/AssertStaticMethodTypeSpecifyingExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php index 9bcca873..3e7f7e12 100644 --- a/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php @@ -24,7 +24,7 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function getClass(): string { - return \PHPUnit\Framework\TestCase::class; + return \PHPUnit\Framework\Assert::class; } public function isStaticMethodSupported( From a0354f884caaa36506c89f300d83fe420070c142 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 15 Apr 2018 02:48:05 +0200 Subject: [PATCH 025/277] Updated PHPStan --- .../Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index be64d2b4..b846b8f5 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -67,10 +67,6 @@ public function testRule(): void 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and \'2\' will always evaluate to false.', 47, ], - [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with stdClass and stdClass will always evaluate to true.', - 48, - ], [ 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\') and array(\'a\', 1) will always evaluate to false.', 51, From f4447e688a8bd1814c31029c92d5859f5c055362 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 6 May 2018 16:22:06 +0200 Subject: [PATCH 026/277] Updated PHPStan --- tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 2 +- .../PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index b846b8f5..f8a66b79 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -11,7 +11,7 @@ class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCa protected function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule(true); + return new ImpossibleCheckTypeMethodCallRule($this->getTypeSpecifier(), true); } /** diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index 61d395b2..80bcc4c3 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -11,7 +11,7 @@ class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\Rule protected function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule(true); + return new ImpossibleCheckTypeStaticMethodCallRule($this->getTypeSpecifier(), true); } /** From 3e7b3ee2b8eabe5bb92c76e44767684d985590b8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 May 2018 14:40:55 +0200 Subject: [PATCH 027/277] Updated PHPStan --- phpstan.neon | 2 +- .../PHPUnit/CreateMockDynamicReturnTypeExtension.php | 8 +++++--- .../GetMockBuilderDynamicReturnTypeExtension.php | 4 +++- .../PHPUnit/MockBuilderDynamicReturnTypeExtension.php | 11 ++++++++--- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 8468a2e5..769e2e80 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ includes: - extension.neon - rules.neon - - vendor/phpstan/phpstan-strict-rules/rules.neon + #- vendor/phpstan/phpstan-strict-rules/rules.neon parameters: excludes_analyse: diff --git a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php b/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php index 1014b37b..9198ab66 100644 --- a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -36,19 +37,20 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { $argumentIndex = $this->methods[$methodReflection->getName()]; + $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); if (!isset($methodCall->args[$argumentIndex])) { - return $methodReflection->getReturnType(); + return $parametersAcceptor->getReturnType(); } $argType = $scope->getType($methodCall->args[$argumentIndex]->value); if (!$argType instanceof ConstantStringType) { - return $methodReflection->getReturnType(); + return $parametersAcceptor->getReturnType(); } $class = $argType->getValue(); return TypeCombinator::intersect( new ObjectType($class), - $methodReflection->getReturnType() + $parametersAcceptor->getReturnType() ); } diff --git a/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php index 78568b11..d2bd4dfe 100644 --- a/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; @@ -24,7 +25,8 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - $mockBuilderType = $methodReflection->getReturnType(); + $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); + $mockBuilderType = $parametersAcceptor->getReturnType(); if (count($methodCall->args) === 0) { return $mockBuilderType; } diff --git a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php index df622f0f..22c828b2 100644 --- a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Broker\Broker; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -25,7 +26,9 @@ public function setBroker(Broker $broker): void public function getClass(): string { $testCase = $this->broker->getClass(\PHPUnit\Framework\TestCase::class); - $mockBuilderType = $testCase->getNativeMethod('getMockBuilder')->getReturnType(); + $mockBuilderType = ParametersAcceptorSelector::selectSingle( + $testCase->getNativeMethod('getMockBuilder')->getVariants() + )->getReturnType(); if (!$mockBuilderType instanceof TypeWithClassName) { throw new \PHPStan\ShouldNotHappenException(); } @@ -52,13 +55,15 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return $calledOnType; } + $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); + if (!$calledOnType instanceof MockBuilderType) { - return $methodReflection->getReturnType(); + return $parametersAcceptor->getReturnType(); } return TypeCombinator::intersect( new ObjectType($calledOnType->getMockedClass()), - $methodReflection->getReturnType() + $parametersAcceptor->getReturnType() ); } From a98cfa84088b50290cf75378fd4a2f7e707745d4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 May 2018 14:45:46 +0200 Subject: [PATCH 028/277] Updated strict-rules --- phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 769e2e80..8468a2e5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ includes: - extension.neon - rules.neon - #- vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon parameters: excludes_analyse: From c6007884a1fb1af65846a0b2f5beaa14614a6126 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 16 May 2018 16:47:14 +0200 Subject: [PATCH 029/277] Updated PHP-Parser to v4 --- composer.json | 3 ++- src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 2 +- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 2 +- src/Rules/PHPUnit/AssertSameWithCountRule.php | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 17b1a165..087ac05d 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "require": { "php": "~7.1", "phpstan/phpstan": "^0.10", - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^7.0", + "nikic/php-parser": "^4.0" }, "require-dev": { "consistence/coding-standard": "^3.0.1", diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 67507b4b..e4ad3503 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -28,7 +28,7 @@ public function processNode(Node $node, Scope $scope): array if (count($node->args) < 2) { return []; } - if (!is_string($node->name) || strtolower($node->name) !== 'assertsame') { + if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { return []; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 318c9479..9d2d9a2d 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -28,7 +28,7 @@ public function processNode(Node $node, Scope $scope): array if (count($node->args) < 2) { return []; } - if (!is_string($node->name) || strtolower($node->name) !== 'assertsame') { + if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { return []; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index d90612e6..9327b8bf 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -27,7 +27,7 @@ public function processNode(Node $node, Scope $scope): array if (count($node->args) < 2) { return []; } - if (!is_string($node->name) || strtolower($node->name) !== 'assertsame') { + if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { return []; } From b308ad4ed5cc4d9dc4eaf0d6de431ff61bfdaad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Unger?= Date: Tue, 22 May 2018 07:21:39 +0200 Subject: [PATCH 030/277] Check composer requirements on Travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index c73cacfe..2ec9a3e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,5 +11,8 @@ before_script: - if [ "$dependencies" = "highest" ]; then composer update --no-interaction; fi; script: - vendor/bin/phing + - > + wget https://github.com/maglnet/ComposerRequireChecker/releases/download/0.2.1/composer-require-checker.phar + && php composer-require-checker.phar check composer.json after_script: - php vendor/bin/coveralls -v From 4a04b40c670d753e90ded5ecdc55be062a60e2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Unger?= Date: Tue, 29 May 2018 10:29:31 +0200 Subject: [PATCH 031/277] Replaced some requirements with conflicts --- composer.json | 7 +++++-- src/Rules/PHPUnit/AssertRuleHelper.php | 2 +- .../PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php | 2 +- .../Assert/AssertStaticMethodTypeSpecifyingExtension.php | 2 +- src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php | 2 +- .../PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php | 2 +- src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php | 2 +- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 087ac05d..2af2dbf9 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,6 @@ "require": { "php": "~7.1", "phpstan/phpstan": "^0.10", - "phpunit/phpunit": "^7.0", "nikic/php-parser": "^4.0" }, "require-dev": { @@ -22,7 +21,11 @@ "phing/phing": "^2.16.0", "phpstan/phpstan-strict-rules": "^0.10", "satooshi/php-coveralls": "^1.0", - "slevomat/coding-standard": "^4.5.2" + "slevomat/coding-standard": "^4.5.2", + "phpunit/phpunit": "^7.0" + }, + "conflict": { + "phpunit/phpunit": "<7.0" }, "autoload": { "psr-4": { diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index ae74086e..e29f86c9 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -11,7 +11,7 @@ class AssertRuleHelper public static function isMethodOrStaticCallOnTestCase(Node $node, Scope $scope): bool { - $testCaseType = new ObjectType(\PHPUnit\Framework\TestCase::class); + $testCaseType = new ObjectType('PHPUnit\Framework\TestCase'); if ($node instanceof Node\Expr\MethodCall) { $calledOnType = $scope->getType($node->var); } elseif ($node instanceof Node\Expr\StaticCall) { diff --git a/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php index 310ebda2..243a7d4f 100644 --- a/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php @@ -24,7 +24,7 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function getClass(): string { - return \PHPUnit\Framework\TestCase::class; + return 'PHPUnit\Framework\TestCase'; } public function isMethodSupported( diff --git a/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php index 3e7f7e12..f33ee7a5 100644 --- a/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php @@ -24,7 +24,7 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function getClass(): string { - return \PHPUnit\Framework\Assert::class; + return 'PHPUnit\Framework\Assert'; } public function isStaticMethodSupported( diff --git a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php b/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php index 9198ab66..b68b010c 100644 --- a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php @@ -26,7 +26,7 @@ class CreateMockDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMetho public function getClass(): string { - return \PHPUnit\Framework\TestCase::class; + return 'PHPUnit\Framework\TestCase'; } public function isMethodSupported(MethodReflection $methodReflection): bool diff --git a/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php index d2bd4dfe..4c7dbe60 100644 --- a/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php @@ -15,7 +15,7 @@ class GetMockBuilderDynamicReturnTypeExtension implements \PHPStan\Type\DynamicM public function getClass(): string { - return \PHPUnit\Framework\TestCase::class; + return 'PHPUnit\Framework\TestCase'; } public function isMethodSupported(MethodReflection $methodReflection): bool diff --git a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php index 22c828b2..b75c69fc 100644 --- a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php @@ -25,7 +25,7 @@ public function setBroker(Broker $broker): void public function getClass(): string { - $testCase = $this->broker->getClass(\PHPUnit\Framework\TestCase::class); + $testCase = $this->broker->getClass('PHPUnit\Framework\TestCase'); $mockBuilderType = ParametersAcceptorSelector::selectSingle( $testCase->getNativeMethod('getMockBuilder')->getVariants() )->getReturnType(); From c8c2af1d78dd74c061227c8362d7435fabd930fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Unger?= Date: Tue, 29 May 2018 11:15:09 +0200 Subject: [PATCH 032/277] Fix tests bootstrap --- tests/bootstrap.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 9dfa9f56..22e480dd 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,7 +1,3 @@ Date: Tue, 29 May 2018 11:15:23 +0200 Subject: [PATCH 033/277] Gitignore tests temp directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ff72e2d0..945ae73d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /composer.lock +/tests/tmp /vendor From 6feecc7faae187daa6be44140cd0f1ba210e6aa0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 22 Jun 2018 20:09:01 +0200 Subject: [PATCH 034/277] Updated PHPStan --- phpstan.neon | 4 ++-- .../Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 3 ++- .../PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 8468a2e5..1ee0dc77 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,7 +9,7 @@ parameters: services: scopeIsInClass: - class: PHPStan\Build\ScopeIsInClassTypeSpecifyingExtension + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension arguments: isInMethodName: isInClass removeNullMethodName: getClassReflection @@ -17,7 +17,7 @@ services: - phpstan.typeSpecifier.methodTypeSpecifyingExtension scopeIsInTrait: - class: PHPStan\Build\ScopeIsInClassTypeSpecifyingExtension + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension arguments: isInMethodName: isInTrait removeNullMethodName: getTraitReflection diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index f8a66b79..dc3bb7e6 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\PHPUnit; +use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule; use PHPStan\Rules\Rule; use PHPStan\Type\PHPUnit\Assert\AssertMethodTypeSpecifyingExtension; @@ -11,7 +12,7 @@ class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCa protected function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule($this->getTypeSpecifier(), true); + return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->getTypeSpecifier()), true); } /** diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index 80bcc4c3..7fbda4f9 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\PHPUnit; +use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule; use PHPStan\Rules\Rule; use PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension; @@ -11,7 +12,7 @@ class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\Rule protected function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule($this->getTypeSpecifier(), true); + return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->getTypeSpecifier()), true); } /** From 9b271426695a552006fe2d48e69925201a083baa Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 1 Nov 2018 16:53:50 +0100 Subject: [PATCH 035/277] Open 0.11-dev --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 2af2dbf9..18110c46 100644 --- a/composer.json +++ b/composer.json @@ -6,12 +6,12 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-master": "0.10-dev" + "dev-master": "0.11-dev" } }, "require": { "php": "~7.1", - "phpstan/phpstan": "^0.10", + "phpstan/phpstan": "^0.11", "nikic/php-parser": "^4.0" }, "require-dev": { @@ -19,7 +19,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", "jakub-onderka/php-parallel-lint": "^1.0", "phing/phing": "^2.16.0", - "phpstan/phpstan-strict-rules": "^0.10", + "phpstan/phpstan-strict-rules": "^0.11", "satooshi/php-coveralls": "^1.0", "slevomat/coding-standard": "^4.5.2", "phpunit/phpunit": "^7.0" From 02750c3f555882a4703c414e33a89eab0462413b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 1 Nov 2018 19:21:30 +0100 Subject: [PATCH 036/277] Fixed build --- tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 2 +- .../PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index dc3bb7e6..95128c4b 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -12,7 +12,7 @@ class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCa protected function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->getTypeSpecifier()), true); + return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier()), true); } /** diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index 7fbda4f9..977b62cb 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -12,7 +12,7 @@ class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\Rule protected function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->getTypeSpecifier()), true); + return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier()), true); } /** From 59e8b6e68f8933eae49ec072502dfe8a0b9fb42c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 1 Nov 2018 20:13:11 +0100 Subject: [PATCH 037/277] Move MockObjectTypeNodeResolverExtension to phpstan-phpunit extension --- extension.neon | 4 ++ .../MockObjectTypeNodeResolverExtension.php | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php diff --git a/extension.neon b/extension.neon index 51c5c8c0..2cf6ef97 100644 --- a/extension.neon +++ b/extension.neon @@ -6,6 +6,10 @@ parameters: - markTestSkipped services: + - + class: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension + tags: + - phpstan.phpDoc.typeNodeResolverExtension - class: PHPStan\Type\PHPUnit\Assert\AssertFunctionTypeSpecifyingExtension tags: diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php new file mode 100644 index 00000000..bd9c2601 --- /dev/null +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -0,0 +1,54 @@ +typeNodeResolver = $typeNodeResolver; + } + + public function getCacheKey(): string + { + return 'phpunit-v1'; + } + + public function resolve(TypeNode $typeNode, \PHPStan\Analyser\NameScope $nameScope): ?Type + { + if (!$typeNode instanceof UnionTypeNode) { + return null; + } + + static $mockClassNames = [ + 'PHPUnit_Framework_MockObject_MockObject' => true, + 'PHPUnit\Framework\MockObject\MockObject' => true, + ]; + + $types = $this->typeNodeResolver->resolveMultiple($typeNode->types, $nameScope); + foreach ($types as $type) { + if (!$type instanceof TypeWithClassName) { + continue; + } + + if (array_key_exists($type->getClassName(), $mockClassNames)) { + return \PHPStan\Type\TypeCombinator::intersect(...$types); + } + } + + return null; + } + +} From bb1b94d3c5f776ddc519fa9f97505b933fdbe5a8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 1 Nov 2018 22:11:07 +0100 Subject: [PATCH 038/277] Fixed build --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 18110c46..25c378d2 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "require": { "php": "~7.1", "phpstan/phpstan": "^0.11", + "phpstan/phpdoc-parser": "^0.3", "nikic/php-parser": "^4.0" }, "require-dev": { From 6c980c114de78c569000679ff19dab7627d63129 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 22 Dec 2018 15:04:18 +0100 Subject: [PATCH 039/277] Fixed build --- phpstan.neon | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpstan.neon b/phpstan.neon index 1ee0dc77..223c2bb1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,6 +6,8 @@ includes: parameters: excludes_analyse: - */tests/*/data/* + ignoreErrors: + - '~^Parameter \#1 \$node \(.*\) of method .*Rule::processNode\(\) should be contravariant with parameter \$node \(PhpParser\\Node\) of method PHPStan\\Rules\\Rule::processNode\(\)$~' services: scopeIsInClass: From 83a0f25e0c5de04e2b9658a763145923660cd1b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sun, 16 Dec 2018 19:26:00 +0100 Subject: [PATCH 040/277] Simplify assertion method name trimming Reducing the number of functions called and avoiding duplicated operations. --- .../PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 507cb252..eefc3662 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -41,13 +41,14 @@ public static function isSupported( private static function trimName(string $name): string { - if (substr($name, 0, strlen('assert')) !== 'assert') { + if (strpos($name, 'assert') !== 0) { return $name; } $name = substr($name, strlen('assert')); - if (substr($name, 0, 3) === 'Not') { - $name = substr($name, 3); + + if (strpos($name, 'Not') === 0) { + return substr($name, 3); } return $name; From 70c22d44b96a21a4952fc13021a5a63cc83f5c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Sun, 16 Dec 2018 19:33:42 +0100 Subject: [PATCH 041/277] Add resolvers for assertions added in PHPUnit 7.5 As documented in PHPUnit's change log the methods `assertInternalType()` and `assertNotInternalType()` have been deprecated and new methods were added as alternatives. More info: https://github.com/sebastianbergmann/phpunit/blob/7.5.0/ChangeLog-7.5.md --- .../AssertTypeSpecifyingExtensionHelper.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index eefc3662..69587866 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -51,6 +51,10 @@ private static function trimName(string $name): string return substr($name, 3); } + if (strpos($name, 'IsNot') === 0) { + return 'Is' . substr($name, 5); + } + return $name; } @@ -148,6 +152,39 @@ private static function getExpressionResolvers(): array new \PhpParser\Node\Expr\ConstFetch(new Name('null')) ); }, + 'IsArray' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_array'), [$actual]); + }, + 'IsBool' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_bool'), [$actual]); + }, + 'IsCallable' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_callable'), [$actual]); + }, + 'IsFloat' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_float'), [$actual]); + }, + 'IsInt' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_int'), [$actual]); + }, + 'IsIterable' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_iterable'), [$actual]); + }, + 'IsNumeric' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_numeric'), [$actual]); + }, + 'IsObject' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_object'), [$actual]); + }, + 'IsResource' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_resource'), [$actual]); + }, + 'IsString' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_string'), [$actual]); + }, + 'IsScalar' => function (Scope $scope, Arg $actual) { + return new \PhpParser\Node\Expr\FuncCall(new Name('is_scalar'), [$actual]); + }, 'InternalType' => function (Scope $scope, Arg $type, Arg $value) { $typeType = $scope->getType($type->value); if (!$typeType instanceof ConstantStringType) { From ec9408e03859f9c775663c14687a7d106e17c751 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 15 Jan 2019 10:45:42 +0100 Subject: [PATCH 042/277] Add missing license file --- LICENSE | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d0053746 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2016 Ondřej Mirtes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + From 93a78ef866faecd566054e1c35550ff80952d420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Unger?= Date: Fri, 10 May 2019 09:22:25 +0200 Subject: [PATCH 043/277] Extension installer support --- composer.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/composer.json b/composer.json index 25c378d2..2a1c12a4 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,18 @@ { "name": "phpstan/phpstan-phpunit", "description": "PHPUnit extensions and rules for PHPStan", + "type": "phpstan-extension", "license": ["MIT"], "minimum-stability": "dev", "prefer-stable": true, "extra": { "branch-alias": { "dev-master": "0.11-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] } }, "require": { From 0d339995c3c6acc56bc912959f436298c70d13ab Mon Sep 17 00:00:00 2001 From: Michael Moravec Date: Fri, 10 May 2019 15:04:08 +0200 Subject: [PATCH 044/277] Fix detection of unspecific assert*() with direct Assert::assert*() calls --- src/Rules/PHPUnit/AssertRuleHelper.php | 4 ++-- src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 2 +- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 2 +- src/Rules/PHPUnit/AssertSameWithCountRule.php | 2 +- tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php | 4 ++++ tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php | 4 ++++ tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php | 4 ++++ tests/Rules/PHPUnit/data/assert-same-boolean-expected.php | 5 +++++ tests/Rules/PHPUnit/data/assert-same-count.php | 5 +++++ tests/Rules/PHPUnit/data/assert-same-null-expected.php | 5 +++++ 10 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index e29f86c9..e8edc506 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -9,9 +9,9 @@ class AssertRuleHelper { - public static function isMethodOrStaticCallOnTestCase(Node $node, Scope $scope): bool + public static function isMethodOrStaticCallOnAssert(Node $node, Scope $scope): bool { - $testCaseType = new ObjectType('PHPUnit\Framework\TestCase'); + $testCaseType = new ObjectType('PHPUnit\Framework\Assert'); if ($node instanceof Node\Expr\MethodCall) { $calledOnType = $scope->getType($node->var); } elseif ($node instanceof Node\Expr\StaticCall) { diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index e4ad3503..cb06ab61 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -21,7 +21,7 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if (!AssertRuleHelper::isMethodOrStaticCallOnTestCase($node, $scope)) { + if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 9d2d9a2d..39f8a992 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -21,7 +21,7 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if (!AssertRuleHelper::isMethodOrStaticCallOnTestCase($node, $scope)) { + if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 9327b8bf..5d0225a5 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -20,7 +20,7 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if (!AssertRuleHelper::isMethodOrStaticCallOnTestCase($node, $scope)) { + if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { return []; } diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php index 63c608ec..ac95c35c 100644 --- a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -31,6 +31,10 @@ public function testRule(): void 'You should use assertFalse() instead of assertSame() when expecting "false"', 17, ], + [ + 'You should use assertTrue() instead of assertSame() when expecting "true"', + 26, + ], ]); } diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php index 53bcf8c0..1c436256 100644 --- a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -23,6 +23,10 @@ public function testRule(): void 'You should use assertNull() instead of assertSame(null, $actual).', 13, ], + [ + 'You should use assertNull() instead of assertSame(null, $actual).', + 24, + ], ]); } diff --git a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php index eee5e0fa..cc17fa75 100644 --- a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php @@ -19,6 +19,10 @@ public function testRule(): void 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', 10, ], + [ + 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', + 20, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php index 22d913e6..1158a574 100644 --- a/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php +++ b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php @@ -21,4 +21,9 @@ public function testAssertSameWithBooleanAsExpected() $this->assertSame($a, 'b'); // OK } + public function testAssertSameIsDetectedWithDirectAssertAccess() + { + \PHPUnit\Framework\Assert::assertSame(true, 'foo'); + } + } diff --git a/tests/Rules/PHPUnit/data/assert-same-count.php b/tests/Rules/PHPUnit/data/assert-same-count.php index 0716ec7c..ab6204f0 100644 --- a/tests/Rules/PHPUnit/data/assert-same-count.php +++ b/tests/Rules/PHPUnit/data/assert-same-count.php @@ -15,4 +15,9 @@ public function testAssertSameWithCountMethodIsOK() $this->assertSame(5, $this->count()); // OK } + public function testAssertSameIsDetectedWithDirectAssertAccess() + { + \PHPUnit\Framework\Assert::assertSame(5, count([1, 2, 3])); + } + } diff --git a/tests/Rules/PHPUnit/data/assert-same-null-expected.php b/tests/Rules/PHPUnit/data/assert-same-null-expected.php index e4cec93e..8c7be339 100644 --- a/tests/Rules/PHPUnit/data/assert-same-null-expected.php +++ b/tests/Rules/PHPUnit/data/assert-same-null-expected.php @@ -19,4 +19,9 @@ public function testAssertSameWithNullAsExpected() $this->assertSame($c, 'foo'); // nullable is OK } + public function testAssertSameIsDetectedWithDirectAssertAccess() + { + \PHPUnit\Framework\Assert::assertSame(null, 'foo'); + } + } From aa4d5fcc0132c832498844b920e8037f18c55e5c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 12 May 2019 18:27:24 +0200 Subject: [PATCH 045/277] Test with bleeding edge --- composer.json | 2 +- phpstan.neon | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2a1c12a4..ac2ba00c 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ }, "require": { "php": "~7.1", - "phpstan/phpstan": "^0.11", + "phpstan/phpstan": "^0.11.4", "phpstan/phpdoc-parser": "^0.3", "nikic/php-parser": "^4.0" }, diff --git a/phpstan.neon b/phpstan.neon index 223c2bb1..9ef50c40 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,7 @@ includes: - extension.neon - rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan/conf/bleedingEdge.neon parameters: excludes_analyse: From fbf2ad56c3b13189d29655e226c9b1da47c2fad9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 17 May 2019 19:50:16 +0200 Subject: [PATCH 046/277] Include rules.neon for phpstan/extension-installer --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ac2ba00c..375251b5 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ }, "phpstan": { "includes": [ - "extension.neon" + "extension.neon", + "rules.neon" ] } }, From b2e743c69d7b643d188146d44c503bf7e8b926f8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 21 May 2019 12:57:19 +0200 Subject: [PATCH 047/277] Installation instructions --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 85fd7a4f..3206d7dc 100644 --- a/README.md +++ b/README.md @@ -66,23 +66,31 @@ public function testSomething() } ``` -## Usage + +## Installation To use this extension, require it in [Composer](https://getcomposer.org/): -```bash +``` composer require --dev phpstan/phpstan-phpunit ``` -And include extension.neon in your project's PHPStan config: +If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set! + +
+ Manual installation + +If you don't want to use `phpstan/extension-installer`, include extension.neon in your project's PHPStan config: ``` includes: - - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/extension.neon ``` To perform framework-specific checks, include also this file: ``` - - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-phpunit/rules.neon ``` + +
From e506c62b901424991baf09934eab3688a68ddc18 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 14 Jun 2019 13:04:10 +0200 Subject: [PATCH 048/277] Open 0.12-dev --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 375251b5..098d82fe 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-master": "0.11-dev" + "dev-master": "0.12-dev" }, "phpstan": { "includes": [ @@ -18,7 +18,7 @@ }, "require": { "php": "~7.1", - "phpstan/phpstan": "^0.11.4", + "phpstan/phpstan": "^0.12", "phpstan/phpdoc-parser": "^0.3", "nikic/php-parser": "^4.0" }, @@ -27,7 +27,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", "jakub-onderka/php-parallel-lint": "^1.0", "phing/phing": "^2.16.0", - "phpstan/phpstan-strict-rules": "^0.11", + "phpstan/phpstan-strict-rules": "^0.12", "satooshi/php-coveralls": "^1.0", "slevomat/coding-standard": "^4.5.2", "phpunit/phpunit": "^7.0" From 593392503b2551bdd5cb9fdbbf0ce3c4aaee0392 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jul 2019 13:04:09 +0200 Subject: [PATCH 049/277] Require phpdoc-parser ^0.4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 098d82fe..04adb4d9 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "require": { "php": "~7.1", "phpstan/phpstan": "^0.12", - "phpstan/phpdoc-parser": "^0.3", + "phpstan/phpdoc-parser": "^0.4", "nikic/php-parser": "^4.0" }, "require-dev": { From d56026ceb01db97a9192d7d407e49bca7604b936 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 12 Nov 2019 06:18:10 +0100 Subject: [PATCH 050/277] Bring extension up to speed --- build.xml | 2 +- composer.json | 4 +- phpcs.xml | 2 + phpstan.neon | 6 +-- .../PHPUnit/AssertSameBooleanExpectedRule.php | 11 +++--- .../PHPUnit/AssertSameNullExpectedRule.php | 11 +++--- src/Rules/PHPUnit/AssertSameWithCountRule.php | 11 +++--- .../AssertTypeSpecifyingExtensionHelper.php | 37 ++++++++++--------- .../AssertSameBooleanExpectedRuleTest.php | 3 ++ ...AssertSameMethodDifferentTypesRuleTest.php | 3 ++ .../AssertSameNullExpectedRuleTest.php | 3 ++ ...SameStaticMethodDifferentTypesRuleTest.php | 3 ++ .../PHPUnit/AssertSameWithCountRuleTest.php | 3 ++ 13 files changed, 60 insertions(+), 39 deletions(-) diff --git a/build.xml b/build.xml index 7b50b9c9..149d1fce 100644 --- a/build.xml +++ b/build.xml @@ -86,7 +86,7 @@ > - + diff --git a/composer.json b/composer.json index 04adb4d9..73753cce 100644 --- a/composer.json +++ b/composer.json @@ -23,13 +23,13 @@ "nikic/php-parser": "^4.0" }, "require-dev": { - "consistence/coding-standard": "^3.0.1", + "consistence/coding-standard": "^3.5", "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", "jakub-onderka/php-parallel-lint": "^1.0", "phing/phing": "^2.16.0", "phpstan/phpstan-strict-rules": "^0.12", "satooshi/php-coveralls": "^1.0", - "slevomat/coding-standard": "^4.5.2", + "slevomat/coding-standard": "^4.7.2", "phpunit/phpunit": "^7.0" }, "conflict": { diff --git a/phpcs.xml b/phpcs.xml index 320dc350..59c8987e 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -24,6 +24,8 @@ @requires "/> + + diff --git a/phpstan.neon b/phpstan.neon index 9ef50c40..9c4be9d3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,13 +2,11 @@ includes: - extension.neon - rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon - - vendor/phpstan/phpstan/conf/bleedingEdge.neon + - phar://phpstan.phar/conf/bleedingEdge.neon parameters: excludes_analyse: - - */tests/*/data/* - ignoreErrors: - - '~^Parameter \#1 \$node \(.*\) of method .*Rule::processNode\(\) should be contravariant with parameter \$node \(PhpParser\\Node\) of method PHPStan\\Rules\\Rule::processNode\(\)$~' + - tests/*/data/* services: scopeIsInClass: diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index cb06ab61..82b7f864 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -6,6 +6,9 @@ use PHPStan\Analyser\Scope; use PHPStan\Type\Constant\ConstantBooleanType; +/** + * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> + */ class AssertSameBooleanExpectedRule implements \PHPStan\Rules\Rule { @@ -14,17 +17,15 @@ public function getNodeType(): string return \PhpParser\NodeAbstract::class; } - /** - * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node - * @param \PHPStan\Analyser\Scope $scope - * @return string[] errors - */ public function processNode(Node $node, Scope $scope): array { if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { return []; } + /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ + $node = $node; + if (count($node->args) < 2) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 39f8a992..b4c17325 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -6,6 +6,9 @@ use PHPStan\Analyser\Scope; use PHPStan\Type\NullType; +/** + * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> + */ class AssertSameNullExpectedRule implements \PHPStan\Rules\Rule { @@ -14,17 +17,15 @@ public function getNodeType(): string return \PhpParser\NodeAbstract::class; } - /** - * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node - * @param \PHPStan\Analyser\Scope $scope - * @return string[] errors - */ public function processNode(Node $node, Scope $scope): array { if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { return []; } + /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ + $node = $node; + if (count($node->args) < 2) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 5d0225a5..98d45051 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -5,6 +5,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +/** + * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> + */ class AssertSameWithCountRule implements \PHPStan\Rules\Rule { @@ -13,17 +16,15 @@ public function getNodeType(): string return \PhpParser\NodeAbstract::class; } - /** - * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node - * @param \PHPStan\Analyser\Scope $scope - * @return string[] errors - */ public function processNode(Node $node, Scope $scope): array { if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { return []; } + /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ + $node = $node; + if (count($node->args) < 2) { return []; } diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 69587866..0ec8e87c 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -3,6 +3,9 @@ namespace PHPStan\Type\PHPUnit\Assert; use PhpParser\Node\Arg; +use PhpParser\Node\Expr\BinaryOp\Identical; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -117,7 +120,7 @@ private static function getExpressionResolvers(): array { if (self::$resolvers === null) { self::$resolvers = [ - 'InstanceOf' => function (Scope $scope, Arg $class, Arg $object) { + 'InstanceOf' => function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { $classType = $scope->getType($class->value); if (!$classType instanceof ConstantStringType) { return null; @@ -128,64 +131,64 @@ private static function getExpressionResolvers(): array new \PhpParser\Node\Name($classType->getValue()) ); }, - 'Same' => function (Scope $scope, Arg $expected, Arg $actual) { + 'Same' => function (Scope $scope, Arg $expected, Arg $actual): Identical { return new \PhpParser\Node\Expr\BinaryOp\Identical( $expected->value, $actual->value ); }, - 'True' => function (Scope $scope, Arg $actual) { + 'True' => function (Scope $scope, Arg $actual): Identical { return new \PhpParser\Node\Expr\BinaryOp\Identical( $actual->value, new \PhpParser\Node\Expr\ConstFetch(new Name('true')) ); }, - 'False' => function (Scope $scope, Arg $actual) { + 'False' => function (Scope $scope, Arg $actual): Identical { return new \PhpParser\Node\Expr\BinaryOp\Identical( $actual->value, new \PhpParser\Node\Expr\ConstFetch(new Name('false')) ); }, - 'Null' => function (Scope $scope, Arg $actual) { + 'Null' => function (Scope $scope, Arg $actual): Identical { return new \PhpParser\Node\Expr\BinaryOp\Identical( $actual->value, new \PhpParser\Node\Expr\ConstFetch(new Name('null')) ); }, - 'IsArray' => function (Scope $scope, Arg $actual) { + 'IsArray' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_array'), [$actual]); }, - 'IsBool' => function (Scope $scope, Arg $actual) { + 'IsBool' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_bool'), [$actual]); }, - 'IsCallable' => function (Scope $scope, Arg $actual) { + 'IsCallable' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_callable'), [$actual]); }, - 'IsFloat' => function (Scope $scope, Arg $actual) { + 'IsFloat' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_float'), [$actual]); }, - 'IsInt' => function (Scope $scope, Arg $actual) { + 'IsInt' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_int'), [$actual]); }, - 'IsIterable' => function (Scope $scope, Arg $actual) { + 'IsIterable' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_iterable'), [$actual]); }, - 'IsNumeric' => function (Scope $scope, Arg $actual) { + 'IsNumeric' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_numeric'), [$actual]); }, - 'IsObject' => function (Scope $scope, Arg $actual) { + 'IsObject' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_object'), [$actual]); }, - 'IsResource' => function (Scope $scope, Arg $actual) { + 'IsResource' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_resource'), [$actual]); }, - 'IsString' => function (Scope $scope, Arg $actual) { + 'IsString' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_string'), [$actual]); }, - 'IsScalar' => function (Scope $scope, Arg $actual) { + 'IsScalar' => function (Scope $scope, Arg $actual): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('is_scalar'), [$actual]); }, - 'InternalType' => function (Scope $scope, Arg $type, Arg $value) { + 'InternalType' => function (Scope $scope, Arg $type, Arg $value): ?FuncCall { $typeType = $scope->getType($type->value); if (!$typeType instanceof ConstantStringType) { return null; diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php index ac95c35c..20af0e1d 100644 --- a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -4,6 +4,9 @@ use PHPStan\Rules\Rule; +/** + * @extends \PHPStan\Testing\RuleTestCase + */ class AssertSameBooleanExpectedRuleTest extends \PHPStan\Testing\RuleTestCase { diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 95128c4b..edb6eca7 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -7,6 +7,9 @@ use PHPStan\Rules\Rule; use PHPStan\Type\PHPUnit\Assert\AssertMethodTypeSpecifyingExtension; +/** + * @extends \PHPStan\Testing\RuleTestCase + */ class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCase { diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php index 1c436256..30ee32e0 100644 --- a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -4,6 +4,9 @@ use PHPStan\Rules\Rule; +/** + * @extends \PHPStan\Testing\RuleTestCase + */ class AssertSameNullExpectedRuleTest extends \PHPStan\Testing\RuleTestCase { diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index 977b62cb..68ca8364 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -7,6 +7,9 @@ use PHPStan\Rules\Rule; use PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension; +/** + * @extends \PHPStan\Testing\RuleTestCase + */ class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCase { diff --git a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php index cc17fa75..cdac1a1f 100644 --- a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php @@ -4,6 +4,9 @@ use PHPStan\Rules\Rule; +/** + * @extends \PHPStan\Testing\RuleTestCase + */ class AssertSameWithCountRuleTest extends \PHPStan\Testing\RuleTestCase { From 79d30f7074ecb773c70147a01478b31efca79023 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 12 Nov 2019 06:24:54 +0100 Subject: [PATCH 051/277] Remove ComposerRequireChecker --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2ec9a3e6..c73cacfe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,8 +11,5 @@ before_script: - if [ "$dependencies" = "highest" ]; then composer update --no-interaction; fi; script: - vendor/bin/phing - - > - wget https://github.com/maglnet/ComposerRequireChecker/releases/download/0.2.1/composer-require-checker.phar - && php composer-require-checker.phar check composer.json after_script: - php vendor/bin/coveralls -v From 846a5fa9d0737757891ccbceff2c63f9773e51c8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 12 Nov 2019 23:50:03 +0100 Subject: [PATCH 052/277] Provide the same functionality in simpler way with stubs --- extension.neon | 11 +-- .../CreateMockDynamicReturnTypeExtension.php | 57 -------------- ...tMockBuilderDynamicReturnTypeExtension.php | 47 ----------- .../MockBuilderDynamicReturnTypeExtension.php | 53 +++---------- src/Type/PHPUnit/MockBuilderType.php | 33 -------- stubs/MockBuilder.php | 29 +++++++ stubs/TestCase.php | 77 +++++++++++++++++++ 7 files changed, 119 insertions(+), 188 deletions(-) delete mode 100644 src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php delete mode 100644 src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php delete mode 100644 src/Type/PHPUnit/MockBuilderType.php create mode 100644 stubs/MockBuilder.php create mode 100644 stubs/TestCase.php diff --git a/extension.neon b/extension.neon index 2cf6ef97..d96bdc7b 100644 --- a/extension.neon +++ b/extension.neon @@ -4,6 +4,9 @@ parameters: - fail - markTestIncomplete - markTestSkipped + stubFiles: + - stubs/MockBuilder.php + - stubs/TestCase.php services: - @@ -22,14 +25,6 @@ services: class: PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension tags: - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension - - - class: PHPStan\Type\PHPUnit\CreateMockDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - - - class: PHPStan\Type\PHPUnit\GetMockBuilderDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - class: PHPStan\Type\PHPUnit\MockBuilderDynamicReturnTypeExtension tags: diff --git a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php b/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php deleted file mode 100644 index b68b010c..00000000 --- a/src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php +++ /dev/null @@ -1,57 +0,0 @@ - 0, - 'createConfiguredMock' => 0, - 'createPartialMock' => 0, - 'createTestProxy' => 0, - 'getMockForAbstractClass' => 0, - 'getMockFromWsdl' => 1, - ]; - - public function getClass(): string - { - return 'PHPUnit\Framework\TestCase'; - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return array_key_exists($methodReflection->getName(), $this->methods); - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - $argumentIndex = $this->methods[$methodReflection->getName()]; - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); - if (!isset($methodCall->args[$argumentIndex])) { - return $parametersAcceptor->getReturnType(); - } - $argType = $scope->getType($methodCall->args[$argumentIndex]->value); - if (!$argType instanceof ConstantStringType) { - return $parametersAcceptor->getReturnType(); - } - - $class = $argType->getValue(); - - return TypeCombinator::intersect( - new ObjectType($class), - $parametersAcceptor->getReturnType() - ); - } - -} diff --git a/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php deleted file mode 100644 index 4c7dbe60..00000000 --- a/src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php +++ /dev/null @@ -1,47 +0,0 @@ -getName() === 'getMockBuilder'; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); - $mockBuilderType = $parametersAcceptor->getReturnType(); - if (count($methodCall->args) === 0) { - return $mockBuilderType; - } - $argType = $scope->getType($methodCall->args[0]->value); - if (!$argType instanceof ConstantStringType) { - return $mockBuilderType; - } - - $class = $argType->getValue(); - - if (!$mockBuilderType instanceof TypeWithClassName) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return new MockBuilderType($mockBuilderType, $class); - } - -} diff --git a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php index b75c69fc..d6ae40d3 100644 --- a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php @@ -4,67 +4,34 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeWithClassName; +use PHPUnit\Framework\MockObject\MockBuilder; -class MockBuilderDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension, \PHPStan\Reflection\BrokerAwareExtension +class MockBuilderDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension { - /** @var \PHPStan\Broker\Broker */ - private $broker; - - public function setBroker(Broker $broker): void - { - $this->broker = $broker; - } - public function getClass(): string { - $testCase = $this->broker->getClass('PHPUnit\Framework\TestCase'); - $mockBuilderType = ParametersAcceptorSelector::selectSingle( - $testCase->getNativeMethod('getMockBuilder')->getVariants() - )->getReturnType(); - if (!$mockBuilderType instanceof TypeWithClassName) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $mockBuilderType->getClassName(); + return MockBuilder::class; } public function isMethodSupported(MethodReflection $methodReflection): bool { - return true; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - $calledOnType = $scope->getType($methodCall->var); - if (!in_array( + return !in_array( $methodReflection->getName(), [ 'getMock', 'getMockForAbstractClass', + 'getMockForTrait', ], true - )) { - return $calledOnType; - } - - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); - - if (!$calledOnType instanceof MockBuilderType) { - return $parametersAcceptor->getReturnType(); - } - - return TypeCombinator::intersect( - new ObjectType($calledOnType->getMockedClass()), - $parametersAcceptor->getReturnType() ); } + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return $scope->getType($methodCall->var); + } + } diff --git a/src/Type/PHPUnit/MockBuilderType.php b/src/Type/PHPUnit/MockBuilderType.php deleted file mode 100644 index e47f16b7..00000000 --- a/src/Type/PHPUnit/MockBuilderType.php +++ /dev/null @@ -1,33 +0,0 @@ -getClassName()); - $this->mockedClass = $mockedClass; - } - - public function getMockedClass(): string - { - return $this->mockedClass; - } - - public function describe(VerbosityLevel $level): string - { - return sprintf('%s<%s>', parent::describe($level), $this->mockedClass); - } - -} diff --git a/stubs/MockBuilder.php b/stubs/MockBuilder.php new file mode 100644 index 00000000..52a0863e --- /dev/null +++ b/stubs/MockBuilder.php @@ -0,0 +1,29 @@ + $type + */ + public function __construct(TestCase $testCase, $type) {} + + /** + * @return MockObject&TMockedClass + */ + public function getMock() {} + + /** + * @return MockObject&TMockedClass + */ + public function getMockForAbstractClass() {} + +} diff --git a/stubs/TestCase.php b/stubs/TestCase.php new file mode 100644 index 00000000..bbe19a56 --- /dev/null +++ b/stubs/TestCase.php @@ -0,0 +1,77 @@ + $originalClassName + * @return MockObject&T + */ + public function createMock($originalClassName) {} + + /** + * @template T + * @param class-string $className + * @return MockBuilder + */ + public function getMockBuilder(string $className) {} + + /** + * @template T + * @param class-string $originalClassName + * @return MockObject&T + */ + public function createConfiguredMock($originalClassName) {} + + /** + * @template T + * @param class-string $originalClassName + * @param string[] $methods + * @return MockObject&T + */ + public function createPartialMock($originalClassName, array $methods) {} + + /** + * @template T + * @param class-string $originalClassName + * @return MockObject&T + */ + public function createTestProxy($originalClassName) {} + + /** + * @template T + * @param class-string $originalClassName + * @param string $mockClassName + * @param bool $callOriginalConstructor + * @param bool $callOriginalClone + * @param bool $callAutoload + * @param string[] $mockedMethods + * @param bool $cloneArguments + * @return MockObject&T + */ + protected function getMockForAbstractClass($originalClassName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = false) {} + + /** + * @template T + * @param string $wsdlFile + * @param class-string $originalClassName + * @param string $mockClassName + * @param bool $callOriginalConstructor + * @param array $options + * @return MockObject&T + */ + protected function getMockFromWsdl($wsdlFile, $originalClassName = '', $mockClassName = '', array $methods = [], $callOriginalConstructor = true, array $options = []) {} + + /** + * @param class-string<\Throwable> $exception + * @return void + */ + public function expectException(string $exception) {} + +} From 8f609f69147772b8b59b0a3bf5e17009112de412 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 13 Nov 2019 08:40:47 +0100 Subject: [PATCH 053/277] phpDoc stub for createStub() --- stubs/TestCase.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stubs/TestCase.php b/stubs/TestCase.php index bbe19a56..50f65451 100644 --- a/stubs/TestCase.php +++ b/stubs/TestCase.php @@ -8,6 +8,13 @@ class TestCase { + /** + * @template T + * @param class-string $originalClassName + * @return MockObject&T + */ + public function createStub($originalClassName) {} + /** * @template T * @param class-string $originalClassName From e5eee4e46b0c868d7526731ab5560b9d953a6966 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 26 Nov 2019 10:44:01 +0100 Subject: [PATCH 054/277] Remove stub for expectException() - no real-world benefit --- stubs/TestCase.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/stubs/TestCase.php b/stubs/TestCase.php index 50f65451..a8fe83cd 100644 --- a/stubs/TestCase.php +++ b/stubs/TestCase.php @@ -75,10 +75,4 @@ protected function getMockForAbstractClass($originalClassName, array $arguments */ protected function getMockFromWsdl($wsdlFile, $originalClassName = '', $mockClassName = '', array $methods = [], $callOriginalConstructor = true, array $options = []) {} - /** - * @param class-string<\Throwable> $exception - * @return void - */ - public function expectException(string $exception) {} - } From a9790e1bee7feea2a9dac7e1f39fc9e6aac0b703 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 27 Nov 2019 17:24:02 +0100 Subject: [PATCH 055/277] Remove dependency on phpdoc-parser, it's included in phpstan/phpstan --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 73753cce..701955ff 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,6 @@ "require": { "php": "~7.1", "phpstan/phpstan": "^0.12", - "phpstan/phpdoc-parser": "^0.4", "nikic/php-parser": "^4.0" }, "require-dev": { From cc14b74ba437bb08ce189aada455946890a8414b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 7 Dec 2019 21:34:22 +0100 Subject: [PATCH 056/277] Prefixed stubs so that IDEs are not confused --- stubs/MockBuilder.php | 8 +++---- stubs/TestCase.php | 54 +++++++++++++++++++++---------------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/stubs/MockBuilder.php b/stubs/MockBuilder.php index 52a0863e..1aaf138c 100644 --- a/stubs/MockBuilder.php +++ b/stubs/MockBuilder.php @@ -11,18 +11,18 @@ class MockBuilder { /** - * @param TestCase $testCase - * @param class-string $type + * @phpstan-param TestCase $testCase + * @phpstan-param class-string $type */ public function __construct(TestCase $testCase, $type) {} /** - * @return MockObject&TMockedClass + * @phpstan-return MockObject&TMockedClass */ public function getMock() {} /** - * @return MockObject&TMockedClass + * @phpstan-return MockObject&TMockedClass */ public function getMockForAbstractClass() {} diff --git a/stubs/TestCase.php b/stubs/TestCase.php index a8fe83cd..bfae52d5 100644 --- a/stubs/TestCase.php +++ b/stubs/TestCase.php @@ -10,68 +10,68 @@ class TestCase /** * @template T - * @param class-string $originalClassName - * @return MockObject&T + * @phpstan-param class-string $originalClassName + * @phpstan-return MockObject&T */ public function createStub($originalClassName) {} /** * @template T - * @param class-string $originalClassName - * @return MockObject&T + * @phpstan-param class-string $originalClassName + * @phpstan-return MockObject&T */ public function createMock($originalClassName) {} /** * @template T - * @param class-string $className - * @return MockBuilder + * @phpstan-param class-string $className + * @phpstan-return MockBuilder */ public function getMockBuilder(string $className) {} /** * @template T - * @param class-string $originalClassName - * @return MockObject&T + * @phpstan-param class-string $originalClassName + * @phpstan-return MockObject&T */ public function createConfiguredMock($originalClassName) {} /** * @template T - * @param class-string $originalClassName - * @param string[] $methods - * @return MockObject&T + * @phpstan-param class-string $originalClassName + * @phpstan-param string[] $methods + * @phpstan-return MockObject&T */ public function createPartialMock($originalClassName, array $methods) {} /** * @template T - * @param class-string $originalClassName - * @return MockObject&T + * @phpstan-param class-string $originalClassName + * @phpstan-return MockObject&T */ public function createTestProxy($originalClassName) {} /** * @template T - * @param class-string $originalClassName - * @param string $mockClassName - * @param bool $callOriginalConstructor - * @param bool $callOriginalClone - * @param bool $callAutoload - * @param string[] $mockedMethods - * @param bool $cloneArguments - * @return MockObject&T + * @phpstan-param class-string $originalClassName + * @phpstan-param string $mockClassName + * @phpstan-param bool $callOriginalConstructor + * @phpstan-param bool $callOriginalClone + * @phpstan-param bool $callAutoload + * @phpstan-param string[] $mockedMethods + * @phpstan-param bool $cloneArguments + * @phpstan-return MockObject&T */ protected function getMockForAbstractClass($originalClassName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = false) {} /** * @template T - * @param string $wsdlFile - * @param class-string $originalClassName - * @param string $mockClassName - * @param bool $callOriginalConstructor - * @param array $options - * @return MockObject&T + * @phpstan-param string $wsdlFile + * @phpstan-param class-string $originalClassName + * @phpstan-param string $mockClassName + * @phpstan-param bool $callOriginalConstructor + * @phpstan-param array $options + * @phpstan-return MockObject&T */ protected function getMockFromWsdl($wsdlFile, $originalClassName = '', $mockClassName = '', array $methods = [], $callOriginalConstructor = true, array $options = []) {} From cf003faa81fb7773d0fc321ac6cbe4dd7f4a5865 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 17 Dec 2019 21:41:59 +0100 Subject: [PATCH 057/277] Add missing MockObject definition --- extension.neon | 1 + stubs/MockObject.php | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 stubs/MockObject.php diff --git a/extension.neon b/extension.neon index d96bdc7b..ad68656a 100644 --- a/extension.neon +++ b/extension.neon @@ -6,6 +6,7 @@ parameters: - markTestSkipped stubFiles: - stubs/MockBuilder.php + - stubs/MockObject.php - stubs/TestCase.php services: diff --git a/stubs/MockObject.php b/stubs/MockObject.php new file mode 100644 index 00000000..b3d6d607 --- /dev/null +++ b/stubs/MockObject.php @@ -0,0 +1,8 @@ + Date: Fri, 20 Dec 2019 08:47:16 +0100 Subject: [PATCH 058/277] Enhancement: Run ergebnis/composer-normalize in check target --- build.xml | 26 ++++++++++++++++++++++++++ composer.json | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/build.xml b/build.xml index 149d1fce..f9d4ee45 100644 --- a/build.xml +++ b/build.xml @@ -5,6 +5,7 @@ composer, lint, cs, + composer-normalize-check, tests, phpstan "/> @@ -20,6 +21,31 @@ + + + + + + + + + + + + + + + Date: Fri, 20 Dec 2019 08:47:55 +0100 Subject: [PATCH 059/277] Fix: Run 'composer normalize' --- composer.json | 52 +++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index daa8a62f..1783590b 100644 --- a/composer.json +++ b/composer.json @@ -1,39 +1,39 @@ { "name": "phpstan/phpstan-phpunit", - "description": "PHPUnit extensions and rules for PHPStan", "type": "phpstan-extension", - "license": ["MIT"], - "minimum-stability": "dev", - "prefer-stable": true, - "extra": { - "branch-alias": { - "dev-master": "0.12-dev" - }, - "phpstan": { - "includes": [ - "extension.neon", - "rules.neon" - ] - } - }, + "description": "PHPUnit extensions and rules for PHPStan", + "license": [ + "MIT" + ], "require": { "php": "~7.1", - "phpstan/phpstan": "^0.12", - "nikic/php-parser": "^4.0" + "nikic/php-parser": "^4.0", + "phpstan/phpstan": "^0.12" + }, + "conflict": { + "phpunit/phpunit": "<7.0" }, "require-dev": { "consistence/coding-standard": "^3.5", "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "ergebnis/composer-normalize": "^2.0.2", "jakub-onderka/php-parallel-lint": "^1.0", "phing/phing": "^2.16.0", "phpstan/phpstan-strict-rules": "^0.12", - "satooshi/php-coveralls": "^1.0", - "slevomat/coding-standard": "^4.7.2", "phpunit/phpunit": "^7.0", - "ergebnis/composer-normalize": "^2.0.2" + "satooshi/php-coveralls": "^1.0", + "slevomat/coding-standard": "^4.7.2" }, - "conflict": { - "phpunit/phpunit": "<7.0" + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } }, "autoload": { "psr-4": { @@ -41,6 +41,10 @@ } }, "autoload-dev": { - "classmap": ["tests/"] - } + "classmap": [ + "tests/" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true } From 63afdc7321c40a0b18ab1729f06515f72dd97d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=B6ller?= Date: Fri, 20 Dec 2019 08:48:31 +0100 Subject: [PATCH 060/277] Enhancement: Keep packages sorted in composer.json --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index 1783590b..8fd567d3 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,9 @@ "satooshi/php-coveralls": "^1.0", "slevomat/coding-standard": "^4.7.2" }, + "config": { + "sort-packages": true + }, "extra": { "branch-alias": { "dev-master": "0.12-dev" From 46a447bc1b88ba809d9c34056c9ea2ca940d6dec Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 20 Dec 2019 14:23:04 +0100 Subject: [PATCH 061/277] Fixed TestCase stub --- stubs/TestCase.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stubs/TestCase.php b/stubs/TestCase.php index bfae52d5..6fe1e1f4 100644 --- a/stubs/TestCase.php +++ b/stubs/TestCase.php @@ -54,6 +54,7 @@ public function createTestProxy($originalClassName) {} /** * @template T * @phpstan-param class-string $originalClassName + * @phpstan-param mixed[] $arguments * @phpstan-param string $mockClassName * @phpstan-param bool $callOriginalConstructor * @phpstan-param bool $callOriginalClone @@ -69,8 +70,9 @@ protected function getMockForAbstractClass($originalClassName, array $arguments * @phpstan-param string $wsdlFile * @phpstan-param class-string $originalClassName * @phpstan-param string $mockClassName + * @phpstan-param string[] $methods * @phpstan-param bool $callOriginalConstructor - * @phpstan-param array $options + * @phpstan-param mixed[] $options * @phpstan-return MockObject&T */ protected function getMockFromWsdl($wsdlFile, $originalClassName = '', $mockClassName = '', array $methods = [], $callOriginalConstructor = true, array $options = []) {} From 2086d8d357852b55b76570472b83c44c3e84b56f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 28 Dec 2019 13:43:29 +0100 Subject: [PATCH 062/277] Fixed build --- tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 2 +- .../PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index edb6eca7..588db262 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -15,7 +15,7 @@ class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCa protected function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier()), true); + return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), []), true); } /** diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index 68ca8364..fbb53d6f 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -15,7 +15,7 @@ class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\Rule protected function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier()), true); + return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), []), true); } /** From 6aa937fc3a10f7a32977fdeb4e125a3ce403c33c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 28 Dec 2019 13:53:46 +0100 Subject: [PATCH 063/277] Fixed build --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8fd567d3..57cc35c9 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require": { "php": "~7.1", "nikic/php-parser": "^4.0", - "phpstan/phpstan": "^0.12" + "phpstan/phpstan": "^0.12.4" }, "conflict": { "phpunit/phpunit": "<7.0" From 30bc1914675235c99918c358bab2550221c3d448 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 1 Jan 2020 18:31:01 +0100 Subject: [PATCH 064/277] Package does not depend on nikic/php-parser --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 57cc35c9..f13c4668 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,6 @@ ], "require": { "php": "~7.1", - "nikic/php-parser": "^4.0", "phpstan/phpstan": "^0.12.4" }, "conflict": { From fe49777a04d8dafcfb3958e3441d9c982a1e40ae Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 3 Jan 2020 11:04:21 +0100 Subject: [PATCH 065/277] Compatibility with stable version of PHPStan --- composer.json | 2 +- tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 2 +- .../PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index f13c4668..42287b9f 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "~7.1", - "phpstan/phpstan": "^0.12.4" + "phpstan/phpstan": "^0.12.3" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 588db262..edb6eca7 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -15,7 +15,7 @@ class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCa protected function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), []), true); + return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier()), true); } /** diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index fbb53d6f..68ca8364 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -15,7 +15,7 @@ class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\Rule protected function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), []), true); + return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier()), true); } /** From 855374ce7483d82fb567a2a1466a7a7f099d58b1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 3 Jan 2020 11:04:56 +0100 Subject: [PATCH 066/277] Revert "Compatibility with stable version of PHPStan" This reverts commit fe49777a04d8dafcfb3958e3441d9c982a1e40ae. --- composer.json | 2 +- tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 2 +- .../PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 42287b9f..f13c4668 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "~7.1", - "phpstan/phpstan": "^0.12.3" + "phpstan/phpstan": "^0.12.4" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index edb6eca7..588db262 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -15,7 +15,7 @@ class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCa protected function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier()), true); + return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), []), true); } /** diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index 68ca8364..fbb53d6f 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -15,7 +15,7 @@ class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\Rule protected function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier()), true); + return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), []), true); } /** From 26394996368b6d033d012547d3197f4e07e23021 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 10 Jan 2020 13:07:21 +0100 Subject: [PATCH 067/277] Renamed stub files extensions to not pollute PhpStorm index --- extension.neon | 6 +++--- stubs/{MockBuilder.php => MockBuilder.stub} | 0 stubs/{MockObject.php => MockObject.stub} | 0 stubs/{TestCase.php => TestCase.stub} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename stubs/{MockBuilder.php => MockBuilder.stub} (100%) rename stubs/{MockObject.php => MockObject.stub} (100%) rename stubs/{TestCase.php => TestCase.stub} (100%) diff --git a/extension.neon b/extension.neon index ad68656a..3395ec1c 100644 --- a/extension.neon +++ b/extension.neon @@ -5,9 +5,9 @@ parameters: - markTestIncomplete - markTestSkipped stubFiles: - - stubs/MockBuilder.php - - stubs/MockObject.php - - stubs/TestCase.php + - stubs/MockBuilder.stub + - stubs/MockObject.stub + - stubs/TestCase.stub services: - diff --git a/stubs/MockBuilder.php b/stubs/MockBuilder.stub similarity index 100% rename from stubs/MockBuilder.php rename to stubs/MockBuilder.stub diff --git a/stubs/MockObject.php b/stubs/MockObject.stub similarity index 100% rename from stubs/MockObject.php rename to stubs/MockObject.stub diff --git a/stubs/TestCase.php b/stubs/TestCase.stub similarity index 100% rename from stubs/TestCase.php rename to stubs/TestCase.stub From b0a6eec0c4373a8eb055331f1db3eafa1bf0148a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 20 Jan 2020 13:23:39 +0100 Subject: [PATCH 068/277] Build compatibility with PHPStan 0.12.6 --- composer.json | 2 +- tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 2 +- .../PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index f13c4668..883bbd0a 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "~7.1", - "phpstan/phpstan": "^0.12.4" + "phpstan/phpstan": "^0.12.6" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 588db262..f840cea5 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -15,7 +15,7 @@ class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCa protected function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), []), true); + return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), [], true), true, true); } /** diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index fbb53d6f..9d1b52e5 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -15,7 +15,7 @@ class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\Rule protected function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), []), true); + return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), [], true), true, true); } /** From 783a04da53f8e2f591f0de25458bad4f16f23fa6 Mon Sep 17 00:00:00 2001 From: Jeroen Noten Date: Fri, 24 Jan 2020 14:57:17 +0100 Subject: [PATCH 069/277] Fix variable name in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3206d7dc..97d7eff3 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ protected function setUp() { $fooMock = $this->createMock(Foo::class); $fooMock->method('doFoo')->will($this->returnValue('test')); - $this->foo = $foo; + $this->foo = $fooMock; } public function testSomething() From e4552cea422ce683ce6ccb5f511b83323727b106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Thu, 2 Apr 2020 16:11:05 +0200 Subject: [PATCH 070/277] Updated Travis CI link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97d7eff3..a7525d4c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PHPStan PHPUnit extensions and rules -[![Build Status](https://travis-ci.org/phpstan/phpstan-phpunit.svg)](https://travis-ci.org/phpstan/phpstan-phpunit) +[![Build Status](https://travis-ci.com/phpstan/phpstan-phpunit.svg?branch=master)](https://travis-ci.com/phpstan/phpstan-phpunit) [![Latest Stable Version](https://poser.pugx.org/phpstan/phpstan-phpunit/v/stable)](https://packagist.org/packages/phpstan/phpstan-phpunit) [![License](https://poser.pugx.org/phpstan/phpstan-phpunit/license)](https://packagist.org/packages/phpstan/phpstan-phpunit) From 3e823ffb21422dd3ac49512bea87b2e1face8b93 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 13 Apr 2020 16:22:08 +0200 Subject: [PATCH 071/277] MockObjectTypeNodeResolverExtension - prioritize UnionType of MockObject|Foo if the intersection is NeverType --- src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php index bd9c2601..b581cf47 100644 --- a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -7,6 +7,7 @@ use PHPStan\PhpDoc\TypeNodeResolverExtension; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; @@ -44,7 +45,10 @@ public function resolve(TypeNode $typeNode, \PHPStan\Analyser\NameScope $nameSco } if (array_key_exists($type->getClassName(), $mockClassNames)) { - return \PHPStan\Type\TypeCombinator::intersect(...$types); + $resultType = \PHPStan\Type\TypeCombinator::intersect(...$types); + if (!$resultType instanceof NeverType) { + return $resultType; + } } } From a6ff54e1f776fa9489a94855bb488c3ae78df6f2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 13 Apr 2020 16:29:39 +0200 Subject: [PATCH 072/277] Fix CS --- src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php index b581cf47..955b8187 100644 --- a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -46,9 +46,11 @@ public function resolve(TypeNode $typeNode, \PHPStan\Analyser\NameScope $nameSco if (array_key_exists($type->getClassName(), $mockClassNames)) { $resultType = \PHPStan\Type\TypeCombinator::intersect(...$types); - if (!$resultType instanceof NeverType) { - return $resultType; + if ($resultType instanceof NeverType) { + continue; } + + return $resultType; } } From f1ba91f189e0c63c6fcf08b7d60dcbc7e2b9aeec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Thu, 16 Apr 2020 18:15:01 +0200 Subject: [PATCH 073/277] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7525d4c..413b94f2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Latest Stable Version](https://poser.pugx.org/phpstan/phpstan-phpunit/v/stable)](https://packagist.org/packages/phpstan/phpstan-phpunit) [![License](https://poser.pugx.org/phpstan/phpstan-phpunit/license)](https://packagist.org/packages/phpstan/phpstan-phpunit) -* [PHPStan](https://github.com/phpstan/phpstan) +* [PHPStan](https://phpstan.org/) * [PHPUnit](https://phpunit.de) This extension provides following features: From a3aeb349ec47cbc4811c69528168ac2feb90c672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Thu, 16 Apr 2020 18:15:29 +0200 Subject: [PATCH 074/277] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 413b94f2..c83efd61 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This extension provides following features: -* `createMock()`, `getMockForAbstractClass()` and `getMockFromWsdl()` methods return an intersection type (see the [detailed explanation of intersection types](https://medium.com/@ondrejmirtes/union-types-vs-intersection-types-fd44a8eacbb)) of the mock object and the mocked class so that both methods from the mock object (like `expects`) and from the mocked class are available on the object. +* `createMock()`, `getMockForAbstractClass()` and `getMockFromWsdl()` methods return an intersection type (see the [detailed explanation of intersection types](https://phpstan.org/blog/union-types-vs-intersection-types)) of the mock object and the mocked class so that both methods from the mock object (like `expects`) and from the mocked class are available on the object. * `getMock()` called on `MockBuilder` is also supported. * Interprets `Foo|PHPUnit_Framework_MockObject_MockObject` in phpDoc so that it results in an intersection type instead of a union type. * Defines early terminating method calls for the `PHPUnit\Framework\TestCase` class to prevent undefined variable errors. From 7232c17e2493dc598173da784477ce0afb2c4e0e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 17 Apr 2020 00:42:04 +0200 Subject: [PATCH 075/277] Add rule for countable --- src/Rules/PHPUnit/AssertSameWithCountRule.php | 16 ++++++++++++++++ .../PHPUnit/AssertSameWithCountRuleTest.php | 6 +++++- .../Rules/PHPUnit/data/assert-same-count.php | 19 ++++++++++++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 98d45051..6e21f537 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Type\ObjectType; /** * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> @@ -44,6 +45,21 @@ public function processNode(Node $node, Scope $scope): array ]; } + if ( + $right instanceof Node\Expr\MethodCall + && $right->name instanceof Node\Identifier + && strtolower($right->name->toString()) === 'count' + && count($right->args) === 0 + ) { + $type = $scope->getType($right->var); + + if ((new ObjectType(\Countable::class))->isSuperTypeOf($type)->yes()) { + return [ + 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).', + ]; + } + } + return []; } diff --git a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php index cdac1a1f..fe3b9ddc 100644 --- a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php @@ -24,7 +24,11 @@ public function testRule(): void ], [ 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', - 20, + 22, + ], + [ + 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).', + 30, ], ]); } diff --git a/tests/Rules/PHPUnit/data/assert-same-count.php b/tests/Rules/PHPUnit/data/assert-same-count.php index ab6204f0..d3e1d6b6 100644 --- a/tests/Rules/PHPUnit/data/assert-same-count.php +++ b/tests/Rules/PHPUnit/data/assert-same-count.php @@ -12,7 +12,9 @@ public function testAssertSameWithCount() public function testAssertSameWithCountMethodIsOK() { - $this->assertSame(5, $this->count()); // OK + $foo = new \stdClass(); + + $this->assertSame(5, $foo->count()); // OK } public function testAssertSameIsDetectedWithDirectAssertAccess() @@ -20,4 +22,19 @@ public function testAssertSameIsDetectedWithDirectAssertAccess() \PHPUnit\Framework\Assert::assertSame(5, count([1, 2, 3])); } + public function testAssertSameWithCountMethodForCountableVariableIsNotOK() + { + $foo = new \stdClass(); + $foo->bar = new Bar (); + + $this->assertSame(5, $foo->bar->count()); + } + } + +class Bar implements \Countable { + public function count() + { + return 1; + } +}; From 1648b3d1689c7e629796b6078f8504e64395574a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 3 May 2020 18:40:21 +0200 Subject: [PATCH 076/277] Fixed build --- composer.json | 2 +- .../PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 883bbd0a..b0cdc1fe 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "~7.1", - "phpstan/phpstan": "^0.12.6" + "phpstan/phpstan": "^0.12.20" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index f840cea5..4f012e02 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -59,10 +59,6 @@ public function testRule(): void 'Call to method PHPUnit\Framework\Assert::assertSame() with 1 and 1 will always evaluate to true.', 44, ], - [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\') and array(\'a\', \'b\') will always evaluate to false.', - 45, - ], [ 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and \'1\' will always evaluate to true.', 46, @@ -71,10 +67,6 @@ public function testRule(): void 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and \'2\' will always evaluate to false.', 47, ], - [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\') and array(\'a\', 1) will always evaluate to false.', - 51, - ], [ 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\', 2, 3.0) and array(\'a\', 1) will always evaluate to false.', 52, From 1d3cfe367482f247ab4ddd3e99cefa91efcbaf91 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 7 May 2020 21:39:50 +0200 Subject: [PATCH 077/277] Report error when trying to configure a non existing method on MockObject --- extension.neon | 9 ++ rules.neon | 1 + src/Rules/PHPUnit/MockMethodCallRule.php | 85 +++++++++++++++++++ ...cationMockerDynamicReturnTypeExtension.php | 29 +++++++ .../MockObjectDynamicReturnTypeExtension.php | 42 +++++++++ stubs/InvocationMocker.stub | 13 +++ .../Rules/PHPUnit/MockMethodCallRuleTest.php | 42 +++++++++ tests/Rules/PHPUnit/data/mock-method-call.php | 43 ++++++++++ 8 files changed, 264 insertions(+) create mode 100644 src/Rules/PHPUnit/MockMethodCallRule.php create mode 100644 src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php create mode 100644 src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php create mode 100644 stubs/InvocationMocker.stub create mode 100644 tests/Rules/PHPUnit/MockMethodCallRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/mock-method-call.php diff --git a/extension.neon b/extension.neon index 3395ec1c..dbedee75 100644 --- a/extension.neon +++ b/extension.neon @@ -5,6 +5,7 @@ parameters: - markTestIncomplete - markTestSkipped stubFiles: + - stubs/InvocationMocker.stub - stubs/MockBuilder.stub - stubs/MockObject.stub - stubs/TestCase.stub @@ -26,7 +27,15 @@ services: class: PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension tags: - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + - + class: PHPStan\Type\PHPUnit\InvocationMockerDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension - class: PHPStan\Type\PHPUnit\MockBuilderDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\PHPUnit\MockObjectDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension diff --git a/rules.neon b/rules.neon index ec02f7d7..a5031f87 100644 --- a/rules.neon +++ b/rules.neon @@ -2,3 +2,4 @@ rules: - PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule - PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule - PHPStan\Rules\PHPUnit\AssertSameWithCountRule + - PHPStan\Rules\PHPUnit\MockMethodCallRule diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php new file mode 100644 index 00000000..8a00af68 --- /dev/null +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -0,0 +1,85 @@ + + */ +class MockMethodCallRule implements \PHPStan\Rules\Rule +{ + + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + /** @var Node\Expr\MethodCall $node */ + $node = $node; + + if (!$node->name instanceof Node\Identifier || $node->name->name !== 'method') { + return []; + } + + if (count($node->args) < 1) { + return []; + } + + $argType = $scope->getType($node->args[0]->value); + if (!($argType instanceof ConstantStringType)) { + return []; + } + + $method = $argType->getValue(); + $type = $scope->getType($node->var); + + if ( + $type instanceof IntersectionType + && in_array(MockObject::class, $type->getReferencedClasses(), true) + && !$type->hasMethod($method)->yes() + ) { + $mockClass = array_filter($type->getReferencedClasses(), function (string $class): bool { + return $class !== MockObject::class; + }); + + return [ + sprintf( + 'Trying to mock an undefined method %s() on class %s.', + $method, + \implode('&', $mockClass) + ), + ]; + } + + if ( + $type instanceof GenericObjectType + && $type->getClassName() === InvocationMocker::class + && count($type->getTypes()) > 0 + ) { + $mockClass = $type->getTypes()[0]; + + if ($mockClass instanceof ObjectType && !$mockClass->hasMethod($method)->yes()) { + return [ + sprintf( + 'Trying to mock an undefined method %s() on class %s.', + $method, + $mockClass->getClassName() + ), + ]; + } + } + + return []; + } + +} diff --git a/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php b/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php new file mode 100644 index 00000000..51e22221 --- /dev/null +++ b/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php @@ -0,0 +1,29 @@ +getName() !== 'getMatcher'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return $scope->getType($methodCall->var); + } + +} diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php new file mode 100644 index 00000000..828fac73 --- /dev/null +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -0,0 +1,42 @@ +getName() === 'expects'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $type = $scope->getType($methodCall->var); + if (!($type instanceof IntersectionType)) { + return new GenericObjectType(InvocationMocker::class, []); + } + + $mockClasses = array_filter($type->getTypes(), function (Type $type): bool { + return !$type instanceof TypeWithClassName || $type->getClassName() !== MockObject::class; + }); + + return new GenericObjectType(InvocationMocker::class, $mockClasses); + } + +} diff --git a/stubs/InvocationMocker.stub b/stubs/InvocationMocker.stub new file mode 100644 index 00000000..c58719f5 --- /dev/null +++ b/stubs/InvocationMocker.stub @@ -0,0 +1,13 @@ + + */ +class MockMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): Rule + { + return new MockMethodCallRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/mock-method-call.php'], [ + [ + 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', + 15, + ], + [ + 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', + 20, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/data/mock-method-call.php b/tests/Rules/PHPUnit/data/mock-method-call.php new file mode 100644 index 00000000..cbe59429 --- /dev/null +++ b/tests/Rules/PHPUnit/data/mock-method-call.php @@ -0,0 +1,43 @@ +createMock(Bar::class)->method('doThing'); + } + + public function testBadMethod() + { + $this->createMock(Bar::class)->method('doBadThing'); + } + + public function testBadMethodWithExpectation() + { + $this->createMock(Bar::class)->expects($this->once())->method('doBadThing'); + } + + public function testWithAnotherObject() + { + $bar = new BarWithMethod(); + $bar->method('doBadThing'); + } + +} + +class Bar { + public function doThing() + { + return 1; + } +}; + +class BarWithMethod { + public function method(string $string) + { + return $string; + } +}; From c56a81014c4e09b81863eb420587f06d65da8d6a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 31 May 2020 00:22:13 +0200 Subject: [PATCH 078/277] Improve CI --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b0cdc1fe..b84a8110 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "jakub-onderka/php-parallel-lint": "^1.0", "phing/phing": "^2.16.0", "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^7.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", "satooshi/php-coveralls": "^1.0", "slevomat/coding-standard": "^4.7.2" }, From 74c1c5f00312e23533fdf579aea71a8343dd3e78 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 31 May 2020 00:28:31 +0200 Subject: [PATCH 079/277] Add phpunit 9 compatibility --- src/Rules/PHPUnit/MockMethodCallRule.php | 2 +- src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index 8a00af68..d8bcd8a6 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -8,7 +8,7 @@ use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; -use PHPUnit\Framework\MockObject\InvocationMocker; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; /** diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php index 828fac73..bd8b00af 100644 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -9,7 +9,7 @@ use PHPStan\Type\IntersectionType; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; -use PHPUnit\Framework\MockObject\InvocationMocker; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; class MockObjectDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension From ab783a8ea634ea23305a8818c4750603e714489b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 1 Jun 2020 18:43:31 +0200 Subject: [PATCH 080/277] Improved MockObjectDynamicReturnTypeExtension --- .../PHPUnit/MockObjectDynamicReturnTypeExtension.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php index bd8b00af..06e69b3d 100644 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -7,6 +7,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; @@ -29,12 +30,16 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method { $type = $scope->getType($methodCall->var); if (!($type instanceof IntersectionType)) { - return new GenericObjectType(InvocationMocker::class, []); + return new ObjectType(InvocationMocker::class); } - $mockClasses = array_filter($type->getTypes(), function (Type $type): bool { + $mockClasses = array_values(array_filter($type->getTypes(), function (Type $type): bool { return !$type instanceof TypeWithClassName || $type->getClassName() !== MockObject::class; - }); + })); + + if (count($mockClasses) !== 1) { + return new ObjectType(InvocationMocker::class); + } return new GenericObjectType(InvocationMocker::class, $mockClasses); } From bcb00a5fc2d78540369738679bbd74673000eb87 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 7 Jul 2020 17:39:15 +0200 Subject: [PATCH 081/277] Update incompatible dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b84a8110..5315ceaf 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ }, "require-dev": { "consistence/coding-standard": "^3.5", - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "ergebnis/composer-normalize": "^2.0.2", "jakub-onderka/php-parallel-lint": "^1.0", "phing/phing": "^2.16.0", From c6acfc9d251562c03f8693cd9127f329ba32ed39 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 16 Jul 2020 11:41:06 +0200 Subject: [PATCH 082/277] TestCase::setUp() as an additional constructor (property initializer) --- composer.json | 2 +- extension.neon | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5315ceaf..71daef8e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "~7.1", - "phpstan/phpstan": "^0.12.20" + "phpstan/phpstan": "^0.12.33" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/extension.neon b/extension.neon index dbedee75..6fb4b77f 100644 --- a/extension.neon +++ b/extension.neon @@ -1,4 +1,6 @@ parameters: + additionalConstructors: + - PHPUnit\Framework\TestCase::setUp earlyTerminatingMethodCalls: PHPUnit\Framework\TestCase: - fail From 92fb18f91ded4981e24862cc34fb19eeced64099 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 21 Jul 2020 16:48:45 +0200 Subject: [PATCH 083/277] Allow PHP 8 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 71daef8e..cff7e3ab 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "MIT" ], "require": { - "php": "~7.1", + "php": "^7.1 || ^8.0", "phpstan/phpstan": "^0.12.33" }, "conflict": { From d686c82965e4e44fb753ca4342fcf797c1f01c88 Mon Sep 17 00:00:00 2001 From: Can Vural Date: Thu, 16 Jul 2020 23:36:05 +0200 Subject: [PATCH 084/277] Add ShouldCallParentMethodsRule --- rules.neon | 1 + .../PHPUnit/ShouldCallParentMethodsRule.php | 104 ++++++++++++++++++ .../ShouldCallParentMethodsRuleTest.php | 32 ++++++ .../data/missing-parent-method-calls.php | 77 +++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 src/Rules/PHPUnit/ShouldCallParentMethodsRule.php create mode 100644 tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/missing-parent-method-calls.php diff --git a/rules.neon b/rules.neon index a5031f87..5be69279 100644 --- a/rules.neon +++ b/rules.neon @@ -3,3 +3,4 @@ rules: - PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule - PHPStan\Rules\PHPUnit\AssertSameWithCountRule - PHPStan\Rules\PHPUnit\MockMethodCallRule + - PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php new file mode 100644 index 00000000..013e1c39 --- /dev/null +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -0,0 +1,104 @@ + + */ +class ShouldCallParentMethodsRule implements \PHPStan\Rules\Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + /** @var InClassMethodNode $node */ + $node = $node; + + if ($scope->getClassReflection() === null) { + return []; + } + + if (!$scope->getClassReflection()->isSubclassOf(TestCase::class)) { + return []; + } + + $parentClass = $scope->getClassReflection()->getParentClass(); + + if ($parentClass === false) { + return []; + } + + if ($parentClass->getName() === TestCase::class) { + return []; + } + + if (!in_array(strtolower($node->getOriginalNode()->name->name), ['setup', 'teardown'], true)) { + return []; + } + + $hasParentCall = $this->hasParentClassCall($node->getOriginalNode()->getStmts()); + + if (!$hasParentCall) { + return [ + RuleErrorBuilder::message( + sprintf('Missing call to parent::%s method.', $node->getOriginalNode()->name->name) + )->build(), + ]; + } + + return []; + } + + /** + * @param Node\Stmt[]|null $stmts + * + * @return bool + */ + private function hasParentClassCall(?array $stmts): bool + { + if ($stmts === null) { + return false; + } + + foreach ($stmts as $stmt) { + if (! $stmt instanceof Node\Stmt\Expression) { + continue; + } + + if (! $stmt->expr instanceof Node\Expr\StaticCall) { + continue; + } + + if (! $stmt->expr->class instanceof Node\Name) { + continue; + } + + $class = (string) $stmt->expr->class; + + if (strtolower($class) !== 'parent') { + continue; + } + + if (! $stmt->expr->name instanceof Node\Identifier) { + continue; + } + + if (in_array(strtolower($stmt->expr->name->name), ['setup', 'teardown'], true)) { + return true; + } + } + + return false; + } + +} diff --git a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php new file mode 100644 index 00000000..eb05b96a --- /dev/null +++ b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php @@ -0,0 +1,32 @@ + + */ +class ShouldCallParentMethodsRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): Rule + { + return new ShouldCallParentMethodsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/missing-parent-method-calls.php'], [ + [ + 'Missing call to parent::setUp method.', + 32, + ], + [ + 'Missing call to parent::tearDown method.', + 63, + ], + ]); + } + +} diff --git a/tests/Rules/PHPUnit/data/missing-parent-method-calls.php b/tests/Rules/PHPUnit/data/missing-parent-method-calls.php new file mode 100644 index 00000000..bd83e6c0 --- /dev/null +++ b/tests/Rules/PHPUnit/data/missing-parent-method-calls.php @@ -0,0 +1,77 @@ +foo = true; + } +} + +class BaseTestCase extends TestCase +{ + public function setUp(): void + { + $this->bar = true; + } + + public function tearDown(): void + { + $this->bar = null; + } +} + +class BazTest extends BaseTestCase +{ + private $baz; + + public function setUp(): void + { + $this->baz = true; + } + + public function baz(): bool + { + return $this->baz; + } +} + +class BarBazTest extends BaseTestCase +{ + public function setUp(): void + { + parent::setUp(); + + $this->barBaz = true; + } +} + +class FooBarBazTest extends BaseTestCase +{ + public function setUp(): void + { + $result = 1 + 1; + parent::setUp(); + + $this->fooBarBaz = $result; + } + + public function tearDown(): void + { + $this->fooBarBaz = null; + } +} + +class NormalBaseClass {} + +class NormalClass extends NormalBaseClass +{ + public function setUp() + { + return true; + } +} From a800264a66e65a89c8e9b1159a1c9d8a2e2567f4 Mon Sep 17 00:00:00 2001 From: Can Vural Date: Mon, 27 Jul 2020 20:38:42 +0200 Subject: [PATCH 085/277] fix code review comments --- rules.neon | 8 ++++++++ .../PHPUnit/ShouldCallParentMethodsRule.php | 16 ++++++++-------- .../PHPUnit/ShouldCallParentMethodsRuleTest.php | 7 +++++-- .../PHPUnit/data/missing-parent-method-calls.php | 2 +- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/rules.neon b/rules.neon index 5be69279..3fac9951 100644 --- a/rules.neon +++ b/rules.neon @@ -1,3 +1,11 @@ +conditionalTags: + PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% + +services: + - + class: PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule + rules: - PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule - PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index 013e1c39..d2e7c565 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -21,9 +21,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - /** @var InClassMethodNode $node */ - $node = $node; - if ($scope->getClassReflection() === null) { return []; } @@ -42,16 +39,18 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!in_array(strtolower($node->getOriginalNode()->name->name), ['setup', 'teardown'], true)) { + $methodName = $node->getOriginalNode()->name->name; + + if (!in_array(strtolower($methodName), ['setup', 'teardown'], true)) { return []; } - $hasParentCall = $this->hasParentClassCall($node->getOriginalNode()->getStmts()); + $hasParentCall = $this->hasParentClassCall($node->getOriginalNode()->getStmts(), strtolower($methodName)); if (!$hasParentCall) { return [ RuleErrorBuilder::message( - sprintf('Missing call to parent::%s method.', $node->getOriginalNode()->name->name) + sprintf('Missing call to parent::%s() method.', $methodName) )->build(), ]; } @@ -61,10 +60,11 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Stmt[]|null $stmts + * @param string $methodName * * @return bool */ - private function hasParentClassCall(?array $stmts): bool + private function hasParentClassCall(?array $stmts, string $methodName): bool { if ($stmts === null) { return false; @@ -93,7 +93,7 @@ private function hasParentClassCall(?array $stmts): bool continue; } - if (in_array(strtolower($stmt->expr->name->name), ['setup', 'teardown'], true)) { + if (strtolower($stmt->expr->name->name) === $methodName) { return true; } } diff --git a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php index eb05b96a..404e63c2 100644 --- a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php +++ b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php @@ -19,11 +19,14 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/missing-parent-method-calls.php'], [ [ - 'Missing call to parent::setUp method.', + 'Missing call to parent::setUp() method.', 32, + ],[ + 'Missing call to parent::setUp() method.', + 55, ], [ - 'Missing call to parent::tearDown method.', + 'Missing call to parent::tearDown() method.', 63, ], ]); diff --git a/tests/Rules/PHPUnit/data/missing-parent-method-calls.php b/tests/Rules/PHPUnit/data/missing-parent-method-calls.php index bd83e6c0..c96b2e29 100644 --- a/tests/Rules/PHPUnit/data/missing-parent-method-calls.php +++ b/tests/Rules/PHPUnit/data/missing-parent-method-calls.php @@ -55,7 +55,7 @@ class FooBarBazTest extends BaseTestCase public function setUp(): void { $result = 1 + 1; - parent::setUp(); + parent::tearDown(); $this->fooBarBaz = $result; } From 20f23c988c38726f2472b61d72ccebe2d2d1bf4c Mon Sep 17 00:00:00 2001 From: Can Vural Date: Tue, 28 Jul 2020 09:51:33 +0200 Subject: [PATCH 086/277] fix code review comments 2 --- rules.neon | 1 - tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rules.neon b/rules.neon index 3fac9951..d7c81ee1 100644 --- a/rules.neon +++ b/rules.neon @@ -11,4 +11,3 @@ rules: - PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule - PHPStan\Rules\PHPUnit\AssertSameWithCountRule - PHPStan\Rules\PHPUnit\MockMethodCallRule - - PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule diff --git a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php index 404e63c2..1400eb4b 100644 --- a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php +++ b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php @@ -21,7 +21,8 @@ public function testRule(): void [ 'Missing call to parent::setUp() method.', 32, - ],[ + ], + [ 'Missing call to parent::setUp() method.', 55, ], From 264abf4c789118c63389bb8009e3418f7cd5fbbd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 29 Jul 2020 22:52:44 +0200 Subject: [PATCH 087/277] Fix logic in ShouldCallParentMethodsRule --- src/Rules/PHPUnit/ShouldCallParentMethodsRule.php | 12 +++++++----- .../PHPUnit/data/missing-parent-method-calls.php | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index d2e7c565..3d9c13ca 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -21,6 +21,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + $methodName = $node->getOriginalNode()->name->name; + if (!in_array(strtolower($methodName), ['setup', 'teardown'], true)) { + return []; + } if ($scope->getClassReflection() === null) { return []; } @@ -34,14 +38,12 @@ public function processNode(Node $node, Scope $scope): array if ($parentClass === false) { return []; } - - if ($parentClass->getName() === TestCase::class) { + if (!$parentClass->hasNativeMethod($methodName)) { return []; } - $methodName = $node->getOriginalNode()->name->name; - - if (!in_array(strtolower($methodName), ['setup', 'teardown'], true)) { + $parentMethod = $parentClass->getNativeMethod($methodName); + if ($parentMethod->getDeclaringClass()->getName() === TestCase::class) { return []; } diff --git a/tests/Rules/PHPUnit/data/missing-parent-method-calls.php b/tests/Rules/PHPUnit/data/missing-parent-method-calls.php index c96b2e29..9aa2525e 100644 --- a/tests/Rules/PHPUnit/data/missing-parent-method-calls.php +++ b/tests/Rules/PHPUnit/data/missing-parent-method-calls.php @@ -75,3 +75,18 @@ public function setUp() return true; } } + +abstract class BaseTestWithoutSetUp extends TestCase +{ + +} + +class LoremTest extends BaseTestWithoutSetUp +{ + + protected function setUp(): void + { + // parent call is not missing here + } + +} From 1dd916d181b0539dea5cd37e91546afb8b107e17 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 28 Jul 2020 10:43:58 -0400 Subject: [PATCH 088/277] Use the parent Assert class for earlyTerminatingMethodCalls --- extension.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension.neon b/extension.neon index 6fb4b77f..fbbd7d69 100644 --- a/extension.neon +++ b/extension.neon @@ -2,7 +2,7 @@ parameters: additionalConstructors: - PHPUnit\Framework\TestCase::setUp earlyTerminatingMethodCalls: - PHPUnit\Framework\TestCase: + PHPUnit\Framework\Assert: - fail - markTestIncomplete - markTestSkipped From 7c54b2d7846066f87260a699679680fe882e1df5 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Thu, 1 Oct 2020 09:58:02 +0200 Subject: [PATCH 089/277] Use the maintained version of php-parallel-lint --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cff7e3ab..6aced8ea 100644 --- a/composer.json +++ b/composer.json @@ -16,8 +16,8 @@ "consistence/coding-standard": "^3.5", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "ergebnis/composer-normalize": "^2.0.2", - "jakub-onderka/php-parallel-lint": "^1.0", "phing/phing": "^2.16.0", + "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-strict-rules": "^0.12", "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", "satooshi/php-coveralls": "^1.0", From e5edd1cfc3c76a3392e006029827e95379d9a0ab Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 21 Oct 2020 15:30:54 +0200 Subject: [PATCH 090/277] Travis > Test on PHP 7.3 and 7.4 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index c73cacfe..3c03ef20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: php php: - 7.1 - 7.2 + - 7.3 + - 7.4 env: - dependencies=lowest - dependencies=highest From 0adbd9f8e6146b91eb9b52492169c83a650bc961 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 21 Oct 2020 16:10:38 +0200 Subject: [PATCH 091/277] Updated coding standard that can run on PHP 7.4 --- composer.json | 4 ++-- phpcs.xml | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 6aced8ea..418d4615 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "phpunit/phpunit": "<7.0" }, "require-dev": { - "consistence/coding-standard": "^3.5", + "consistence/coding-standard": "^3.10", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "ergebnis/composer-normalize": "^2.0.2", "phing/phing": "^2.16.0", @@ -21,7 +21,7 @@ "phpstan/phpstan-strict-rules": "^0.12", "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", "satooshi/php-coveralls": "^1.0", - "slevomat/coding-standard": "^4.7.2" + "slevomat/coding-standard": "^6.4" }, "config": { "sort-packages": true diff --git a/phpcs.xml b/phpcs.xml index 59c8987e..bf73d42f 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -17,7 +17,7 @@ - + + + + + + + + + + + + + - + From ef674b84b6ddc8054829e589e0b0bee5b4ea32a2 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 21 Oct 2020 16:10:42 +0200 Subject: [PATCH 092/277] Bump minimum versions to prevent deprecations while running in lowest mode --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 418d4615..6a1ad2d5 100644 --- a/composer.json +++ b/composer.json @@ -16,10 +16,10 @@ "consistence/coding-standard": "^3.10", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "ergebnis/composer-normalize": "^2.0.2", - "phing/phing": "^2.16.0", + "phing/phing": "^2.16.2", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "phpunit/phpunit": "^7.5.18", "satooshi/php-coveralls": "^1.0", "slevomat/coding-standard": "^6.4" }, From 7bd24abb5eed4868c542a204c3dd8ae2739d29b1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Dec 2020 13:04:49 +0100 Subject: [PATCH 093/277] Use GitHub Actions instead of Travis CI --- .coveralls.yml | 3 - .github/workflows/build.yml | 160 ++++++++++++++++++++++++++++++++++++ .travis.yml | 17 ---- README.md | 2 +- build-cs/.gitignore | 2 + build-cs/composer.json | 7 ++ build.xml | 15 +++- composer.json | 16 ++-- phpcs.xml | 2 +- 9 files changed, 191 insertions(+), 33 deletions(-) delete mode 100644 .coveralls.yml create mode 100644 .github/workflows/build.yml delete mode 100644 .travis.yml create mode 100644 build-cs/.gitignore create mode 100644 build-cs/composer.json diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 267998da..00000000 --- a/.coveralls.yml +++ /dev/null @@ -1,3 +0,0 @@ -service_name: travis-ci -coverage_clover: tests/tmp/clover.xml -json_path: tests/tmp/coveralls.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..aba91f05 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,160 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Build" + +on: + pull_request: + push: + branches: + - "master" + +jobs: + lint: + name: "Lint" + runs-on: "ubuntu-latest" + + strategy: + matrix: + php-version: + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "8.0" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Validate Composer" + run: "composer validate" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress --no-suggest" + + - name: "Update PHPUnit" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' + run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" + + + - name: "Lint" + run: "vendor/bin/phing lint" + + coding-standards: + name: "Coding Standard" + + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "7.4" + + - name: "Validate Composer" + run: "composer validate" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress --no-suggest" + + - name: "Lint" + run: "vendor/bin/phing lint" + + - name: "Coding Standard" + run: "vendor/bin/phing cs" + + tests: + name: "Tests" + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: + php-version: + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "8.0" + dependencies: + - "lowest" + - "highest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --no-interaction --no-progress --no-suggest" + + - name: "Update PHPUnit" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' + run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" + + - name: "Tests" + run: "vendor/bin/phing tests" + + static-analysis: + name: "PHPStan" + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: + php-version: + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "8.0" + dependencies: + - "lowest" + - "highest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: mbstring + tools: composer:v2 + + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --no-interaction --no-progress --no-suggest" + + - name: "Update PHPUnit" + if: matrix.php-version == '7.4' || matrix.php-version == '8.0' + run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" + + - name: "PHPStan" + run: "vendor/bin/phing phpstan" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3c03ef20..00000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: php -php: - - 7.1 - - 7.2 - - 7.3 - - 7.4 -env: - - dependencies=lowest - - dependencies=highest -before_script: - - composer self-update - - if [ "$dependencies" = "lowest" ]; then composer update --prefer-lowest --no-interaction; fi; - - if [ "$dependencies" = "highest" ]; then composer update --no-interaction; fi; -script: - - vendor/bin/phing -after_script: - - php vendor/bin/coveralls -v diff --git a/README.md b/README.md index c83efd61..205cbe4b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PHPStan PHPUnit extensions and rules -[![Build Status](https://travis-ci.com/phpstan/phpstan-phpunit.svg?branch=master)](https://travis-ci.com/phpstan/phpstan-phpunit) +[![Build](https://github.com/phpstan/phpstan-phpunit/workflows/Build/badge.svg)](https://github.com/phpstan/phpstan-phpunit/actions) [![Latest Stable Version](https://poser.pugx.org/phpstan/phpstan-phpunit/v/stable)](https://packagist.org/packages/phpstan/phpstan-phpunit) [![License](https://poser.pugx.org/phpstan/phpstan-phpunit/license)](https://packagist.org/packages/phpstan/phpstan-phpunit) diff --git a/build-cs/.gitignore b/build-cs/.gitignore new file mode 100644 index 00000000..ff72e2d0 --- /dev/null +++ b/build-cs/.gitignore @@ -0,0 +1,2 @@ +/composer.lock +/vendor diff --git a/build-cs/composer.json b/build-cs/composer.json new file mode 100644 index 00000000..9acd0275 --- /dev/null +++ b/build-cs/composer.json @@ -0,0 +1,7 @@ +{ + "require-dev": { + "consistence/coding-standard": "^3.10", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "slevomat/coding-standard": "^6.4" + } +} diff --git a/build.xml b/build.xml index f9d4ee45..aaadd35c 100644 --- a/build.xml +++ b/build.xml @@ -60,7 +60,18 @@ + + + + + + - + From 432575b41cf2d4f44e460234acaf56119ed97d36 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Dec 2020 13:12:51 +0100 Subject: [PATCH 094/277] Automatic release workflow --- .github/workflows/release.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..e04c2563 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Create release" + +on: + push: + tags: + - '*' + +jobs: + deploy: + name: "Deploy" + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: Generate changelog + id: changelog + uses: metcalfc/changelog-generator@v0.4.4 + with: + myToken: ${{ secrets.GITHUB_TOKEN }} + + - name: "Create release" + id: create-release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + body: ${{ steps.changelog.outputs.changelog }} From 58e52ad987ba571f69a8710eb907ad68e93ad977 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 19 Dec 2020 16:09:49 +0100 Subject: [PATCH 095/277] Dependabot --- .github/dependabot.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..15b77335 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: composer + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 10 + - package-ecosystem: composer + directory: "/build-cs" + schedule: + interval: monthly + open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 10 From ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 5 Mar 2021 13:38:52 +0100 Subject: [PATCH 096/277] Allow toggling union to intersection type conversion --- extension.neon | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/extension.neon b/extension.neon index fbbd7d69..363d7aca 100644 --- a/extension.neon +++ b/extension.neon @@ -1,4 +1,6 @@ parameters: + phpunit: + convertUnionToIntersectionType: true additionalConstructors: - PHPUnit\Framework\TestCase::setUp earlyTerminatingMethodCalls: @@ -12,11 +14,14 @@ parameters: - stubs/MockObject.stub - stubs/TestCase.stub +parametersSchema: + phpunit: structure([ + convertUnionToIntersectionType: bool() + ]) + services: - class: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension - tags: - - phpstan.phpDoc.typeNodeResolverExtension - class: PHPStan\Type\PHPUnit\Assert\AssertFunctionTypeSpecifyingExtension tags: @@ -41,3 +46,7 @@ services: class: PHPStan\Type\PHPUnit\MockObjectDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + +conditionalTags: + PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: + phpstan.phpDoc.typeNodeResolverExtension: %phpunit.convertUnionToIntersectionType% From 25fe53e5e8ca274a89bb3fe34bb83263c787982e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 2 Apr 2021 16:57:30 +0200 Subject: [PATCH 097/277] Fix build --- .../AssertSameBooleanExpectedRuleTest.php | 10 +++++++++ ...AssertSameMethodDifferentTypesRuleTest.php | 21 +++++++++---------- .../AssertSameNullExpectedRuleTest.php | 10 +++++++++ ...SameStaticMethodDifferentTypesRuleTest.php | 21 +++++++++---------- .../PHPUnit/AssertSameWithCountRuleTest.php | 10 +++++++++ .../ShouldCallParentMethodsRuleTest.php | 10 +++++++++ 6 files changed, 60 insertions(+), 22 deletions(-) diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php index 20af0e1d..a96aa0a1 100644 --- a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -41,4 +41,14 @@ public function testRule(): void ]); } + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + } diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 4f012e02..d32ee2b1 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule; use PHPStan\Rules\Rule; -use PHPStan\Type\PHPUnit\Assert\AssertMethodTypeSpecifyingExtension; /** * @extends \PHPStan\Testing\RuleTestCase @@ -18,16 +17,6 @@ protected function getRule(): Rule return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), [], true), true, true); } - /** - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] - */ - protected function getMethodTypeSpecifyingExtensions(): array - { - return [ - new AssertMethodTypeSpecifyingExtension(), - ]; - } - public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same.php'], [ @@ -74,4 +63,14 @@ public function testRule(): void ]); } + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + } diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php index 30ee32e0..009a96d0 100644 --- a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -33,4 +33,14 @@ public function testRule(): void ]); } + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + } diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index 9d1b52e5..524f76e2 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule; use PHPStan\Rules\Rule; -use PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension; /** * @extends \PHPStan\Testing\RuleTestCase @@ -18,16 +17,6 @@ protected function getRule(): Rule return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), [], true), true, true); } - /** - * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] - */ - protected function getStaticMethodTypeSpecifyingExtensions(): array - { - return [ - new AssertStaticMethodTypeSpecifyingExtension(), - ]; - } - public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-same.php'], [ @@ -58,4 +47,14 @@ public function testRule(): void ]); } + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + } diff --git a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php index fe3b9ddc..9a0caf91 100644 --- a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php @@ -33,4 +33,14 @@ public function testRule(): void ]); } + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + } diff --git a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php index 1400eb4b..3f8d6e2d 100644 --- a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php +++ b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php @@ -33,4 +33,14 @@ public function testRule(): void ]); } + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + } From 07cbf74439418b07752260d6e6a7b2159f2b3a19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Apr 2021 05:29:45 +0000 Subject: [PATCH 098/277] Bump metcalfc/changelog-generator from v0.4.4 to v1.0.0 Bumps [metcalfc/changelog-generator](https://github.com/metcalfc/changelog-generator) from v0.4.4 to v1.0.0. - [Release notes](https://github.com/metcalfc/changelog-generator/releases) - [Changelog](https://github.com/metcalfc/changelog-generator/blob/main/release-notes.png) - [Commits](https://github.com/metcalfc/changelog-generator/compare/v0.4.4...e5306b306fa2e34f05258789e0e5c526c1bd4352) Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e04c2563..225470a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v0.4.4 + uses: metcalfc/changelog-generator@v1.0.0 with: myToken: ${{ secrets.GITHUB_TOKEN }} From 527d8e136c02b47211b4e59bd48b28e3ba8b204b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 3 Apr 2021 15:31:02 +0200 Subject: [PATCH 099/277] Require PHPStan 0.12.83 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f1507fc3..bec88ce3 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.60" + "phpstan/phpstan": "^0.12.83" }, "conflict": { "phpunit/phpunit": "<7.0" From 3123fec79d329725a7fd4649d32fd95b36b0040a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 30 Apr 2021 09:25:16 +0200 Subject: [PATCH 100/277] Lock closed issues --- .github/workflows/lock-closed-issues.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/lock-closed-issues.yml diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml new file mode 100644 index 00000000..960c1ba5 --- /dev/null +++ b/.github/workflows/lock-closed-issues.yml @@ -0,0 +1,23 @@ +name: 'Lock Issues' + +on: + schedule: + - cron: '0 0 * * *' + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v2 + with: + github-token: ${{ github.token }} + issue-lock-inactive-days: '31' + issue-exclude-created-before: '' + issue-exclude-labels: '' + issue-lock-labels: '' + issue-lock-comment: > + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + issue-lock-reason: 'resolved' + process-only: 'issues' From 52f7072ddc5f81492f9d2de65a24813a48c90b18 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 30 Apr 2021 13:10:37 +0200 Subject: [PATCH 101/277] Unchecked exceptions configuration --- composer.json | 2 +- extension.neon | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bec88ce3..97de682a 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.83" + "phpstan/phpstan": "^0.12.86" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/extension.neon b/extension.neon index 363d7aca..10254483 100644 --- a/extension.neon +++ b/extension.neon @@ -13,6 +13,10 @@ parameters: - stubs/MockBuilder.stub - stubs/MockObject.stub - stubs/TestCase.stub + exceptions: + uncheckedExceptionRegexes: + - '#^PHPUnit\\#' + - '#^SebastianBergmann\\#' parametersSchema: phpunit: structure([ From 534606b1a4c7324cf5dba1b16fe55cf5af425fb1 Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Fri, 14 May 2021 11:18:39 +0200 Subject: [PATCH 102/277] Drop Phing for Makefile See https://github.com/phpstan/phpstan-symfony/pull/160 --- .gitattributes | 14 +++- .github/workflows/build.yml | 10 +-- .gitignore | 3 +- Makefile | 23 +++++++ build-cs/composer.json | 2 +- build.xml | 134 ------------------------------------ composer.json | 1 - phpcs.xml | 11 ++- phpunit.xml | 36 ++++++++++ tests/phpunit.xml | 27 -------- tmp/.gitignore | 3 + tmp/cache/.gitignore | 2 + 12 files changed, 95 insertions(+), 171 deletions(-) create mode 100644 Makefile delete mode 100644 build.xml create mode 100644 phpunit.xml delete mode 100644 tests/phpunit.xml create mode 100644 tmp/.gitignore create mode 100644 tmp/cache/.gitignore diff --git a/.gitattributes b/.gitattributes index 18e14aad..615bf05f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,13 @@ -/tests export-ignore +*.php text eol=lf +*.stub linguist-language=PHP +*.neon linguist-language=YAML + +.github export-ignore +tests export-ignore +tmp export-ignore +.gitattributes export-ignore +.gitignore export-ignore +Makefile export-ignore +phpcs.xml export-ignore +phpstan.neon export-ignore +phpunit.xml export-ignore diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aba91f05..3cf185e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: - name: "Lint" - run: "vendor/bin/phing lint" + run: "make lint" coding-standards: name: "Coding Standard" @@ -68,10 +68,10 @@ jobs: run: "composer install --no-interaction --no-progress --no-suggest" - name: "Lint" - run: "vendor/bin/phing lint" + run: "make lint" - name: "Coding Standard" - run: "vendor/bin/phing cs" + run: "make cs" tests: name: "Tests" @@ -113,7 +113,7 @@ jobs: run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" - name: "Tests" - run: "vendor/bin/phing tests" + run: "make tests" static-analysis: name: "PHPStan" @@ -157,4 +157,4 @@ jobs: run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" - name: "PHPStan" - run: "vendor/bin/phing phpstan" + run: "make phpstan" diff --git a/.gitignore b/.gitignore index 945ae73d..d6a83e59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -/composer.lock /tests/tmp /vendor +composer.lock +.phpunit.result.cache diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..fe917d3b --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: check +check: lint cs tests phpstan + +.PHONY: tests +tests: + php vendor/bin/phpunit + +.PHONY: lint +lint: + php vendor/bin/parallel-lint --colors \ + src tests + +.PHONY: cs +cs: + composer install --working-dir build-cs && php build-cs/vendor/bin/phpcs + +.PHONY: cs-fix +cs-fix: + php build-cs/vendor/bin/phpcbf + +.PHONY: phpstan +phpstan: + php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests diff --git a/build-cs/composer.json b/build-cs/composer.json index 9acd0275..ed7744e1 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -1,6 +1,6 @@ { "require-dev": { - "consistence/coding-standard": "^3.10", + "consistence-community/coding-standard": "^3.10", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "slevomat/coding-standard": "^6.4" } diff --git a/build.xml b/build.xml deleted file mode 100644 index aaadd35c..00000000 --- a/build.xml +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/composer.json b/composer.json index 97de682a..9941d096 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,6 @@ }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", - "phing/phing": "^2.16.3", "phpstan/phpstan-strict-rules": "^0.12.6", "phpunit/phpunit": "^7.5.20" }, diff --git a/phpcs.xml b/phpcs.xml index 0c47d815..ce6e811c 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,14 @@ - + + + + + + + src + tests + @@ -52,5 +60,6 @@ + tests/tmp tests/*/data diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..8d53d3fd --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,36 @@ + + + + + ./src + + + + + + + + + + tests + + + + + diff --git a/tests/phpunit.xml b/tests/phpunit.xml deleted file mode 100644 index 1a84817e..00000000 --- a/tests/phpunit.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - ../src - - - - - - - diff --git a/tmp/.gitignore b/tmp/.gitignore new file mode 100644 index 00000000..37890cae --- /dev/null +++ b/tmp/.gitignore @@ -0,0 +1,3 @@ +* +!cache +!.* diff --git a/tmp/cache/.gitignore b/tmp/cache/.gitignore new file mode 100644 index 00000000..125e3429 --- /dev/null +++ b/tmp/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.* From 82b919af1021a03548badde76eb690f5cb7f4b6c Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Sun, 16 May 2021 08:25:30 +0200 Subject: [PATCH 103/277] chore: Use the latest PHPUnit wherever possible, use PHP 8.0 for PHPCS See https://github.com/phpstan/phpstan-symfony/pull/162 --- .github/workflows/build.yml | 21 ++++++++++----------- composer.json | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3cf185e4..b693c051 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,10 +38,9 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress --no-suggest" - - name: "Update PHPUnit" - if: matrix.php-version == '7.4' || matrix.php-version == '8.0' - run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" - + - name: "Downgrade PHPUnit" + if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - name: "Lint" run: "make lint" @@ -59,7 +58,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "7.4" + php-version: "8.0" - name: "Validate Composer" run: "composer validate" @@ -108,9 +107,9 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress --no-suggest" - - name: "Update PHPUnit" - if: matrix.php-version == '7.4' || matrix.php-version == '8.0' - run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" + - name: "Downgrade PHPUnit" + if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - name: "Tests" run: "make tests" @@ -152,9 +151,9 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress --no-suggest" - - name: "Update PHPUnit" - if: matrix.php-version == '7.4' || matrix.php-version == '8.0' - run: "composer require --dev phpunit/phpunit:'^9.5' --update-with-dependencies" + - name: "Downgrade PHPUnit" + if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - name: "PHPStan" run: "make phpstan" diff --git a/composer.json b/composer.json index 9941d096..51cd691a 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-strict-rules": "^0.12.6", - "phpunit/phpunit": "^7.5.20" + "phpunit/phpunit": "^9.5" }, "config": { "platform": { From 151d71b28f3d1b8aa517fc44bcb6ff2789153016 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 9 Jun 2021 15:37:16 +0200 Subject: [PATCH 104/277] Rewrite in backward compatibility compliant way --- .../Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 4 ++-- .../PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index d32ee2b1..072603e1 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\PHPUnit; -use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule; use PHPStan\Rules\Rule; @@ -14,7 +13,7 @@ class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCa protected function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), [], true), true, true); + return self::getContainer()->getByType(ImpossibleCheckTypeMethodCallRule::class); } public function testRule(): void @@ -70,6 +69,7 @@ public static function getAdditionalConfigFiles(): array { return [ __DIR__ . '/../../../extension.neon', + __DIR__ . '/../../../vendor/phpstan/phpstan-strict-rules/rules.neon', ]; } diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index 524f76e2..e55d3e22 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\PHPUnit; -use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule; use PHPStan\Rules\Rule; @@ -14,7 +13,7 @@ class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\Rule protected function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule(new ImpossibleCheckTypeHelper($this->createBroker(), $this->getTypeSpecifier(), [], true), true, true); + return self::getContainer()->getByType(ImpossibleCheckTypeStaticMethodCallRule::class); } public function testRule(): void @@ -54,6 +53,7 @@ public static function getAdditionalConfigFiles(): array { return [ __DIR__ . '/../../../extension.neon', + __DIR__ . '/../../../vendor/phpstan/phpstan-strict-rules/rules.neon', ]; } From 8e11b1a1d0c8e0a7be3e49715c99a44c54b16803 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 17 Jun 2021 10:22:07 +0200 Subject: [PATCH 105/277] Make sure AssertFunctionTypeSpecifyingExtension works with namespaced functions --- .../AssertFunctionTypeSpecifyingExtension.php | 14 ++++++-- ...ertFunctionTypeSpecifyingExtensionTest.php | 36 +++++++++++++++++++ tests/Type/PHPUnit/data/assert-function.php | 20 +++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php create mode 100644 tests/Type/PHPUnit/data/assert-function.php diff --git a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php index 6a684d36..d4ec8fb1 100644 --- a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php @@ -29,7 +29,7 @@ public function isFunctionSupported( ): bool { return AssertTypeSpecifyingExtensionHelper::isSupported( - $functionReflection->getName(), + $this->trimName($functionReflection->getName()), $node->args ); } @@ -44,9 +44,19 @@ public function specifyTypes( return AssertTypeSpecifyingExtensionHelper::specifyTypes( $this->typeSpecifier, $scope, - $functionReflection->getName(), + $this->trimName($functionReflection->getName()), $node->args ); } + private function trimName(string $functionName): string + { + $prefix = 'PHPUnit\\Framework\\'; + if (strpos($functionName, $prefix) === 0) { + return substr($functionName, strlen($prefix)); + } + + return $functionName; + } + } diff --git a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php new file mode 100644 index 00000000..e1984cf0 --- /dev/null +++ b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/assert-function.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param string $assertType + * @param string $file + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/../../../extension.neon']; + } + +} diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php new file mode 100644 index 00000000..5c1cc96c --- /dev/null +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -0,0 +1,20 @@ + Date: Thu, 17 Jun 2021 10:28:30 +0200 Subject: [PATCH 106/277] Fix --- .../PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php index e1984cf0..c32b9d0a 100644 --- a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php @@ -10,6 +10,10 @@ class AssertFunctionTypeSpecifyingExtensionTest extends TypeInferenceTestCase /** @return mixed[] */ public function dataFileAsserts(): iterable { + if (!function_exists('PHPUnit\\Framework\\assertInstanceOf')) { + return []; + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-function.php'); } From 6aaff1196c4f808769774b49a94a60e5fdf18de7 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 14 Jul 2021 12:42:31 +0200 Subject: [PATCH 107/277] Add support for assertArrayHasKey (#100) --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 3 +++ tests/Type/PHPUnit/data/assert-function.php | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 0ec8e87c..8a56f324 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -252,6 +252,9 @@ private static function getExpressionResolvers(): array ] ); }, + 'ArrayHasKey' => function (Scope $scope, Arg $key, Arg $array): FuncCall { + return new \PhpParser\Node\Expr\FuncCall(new Name('array_key_exists'), [$key, $array]); + }, ]; } diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 5c1cc96c..f31fb378 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -3,6 +3,8 @@ namespace AssertFunction; use function PHPStan\Testing\assertType; +use function PHPUnit\Framework\assertArrayHasKey; +use function PHPUnit\Framework\assertArrayNotHasKey; use function PHPUnit\Framework\assertInstanceOf; class Foo @@ -17,4 +19,15 @@ public function doFoo($o): void assertType(self::class, $o); } + public function arrayHasNumericKey(array $a): void { + assertArrayHasKey(0, $a); + assertType('array&hasOffset(0)', $a); + } + + public function arrayHasStringKey(array $a): void + { + assertArrayHasKey('key', $a); + assertType("array&hasOffset('key')", $a); + } + } From 2968a62ddfec40e72b7218ede79717ce836aa06e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 20 Jul 2021 11:32:23 +0200 Subject: [PATCH 108/277] Fix PHP 8.1 compatibility --- .github/workflows/build.yml | 3 +++ composer.json | 2 +- tests/Rules/PHPUnit/data/assert-same-count.php | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b693c051..59b79ed7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,7 @@ jobs: - "7.3" - "7.4" - "8.0" + - "8.1" steps: - name: "Checkout" @@ -85,6 +86,7 @@ jobs: - "7.3" - "7.4" - "8.0" + - "8.1" dependencies: - "lowest" - "highest" @@ -127,6 +129,7 @@ jobs: - "7.3" - "7.4" - "8.0" + - "8.1" dependencies: - "lowest" - "highest" diff --git a/composer.json b/composer.json index 51cd691a..24d74cc0 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.86" + "phpstan/phpstan": "^0.12.92" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/tests/Rules/PHPUnit/data/assert-same-count.php b/tests/Rules/PHPUnit/data/assert-same-count.php index d3e1d6b6..73df333d 100644 --- a/tests/Rules/PHPUnit/data/assert-same-count.php +++ b/tests/Rules/PHPUnit/data/assert-same-count.php @@ -33,7 +33,7 @@ public function testAssertSameWithCountMethodForCountableVariableIsNotOK() } class Bar implements \Countable { - public function count() + public function count(): int { return 1; } From 7c01ef93bf128b4ac8bdad38c54b2a4fd6b0b3cc Mon Sep 17 00:00:00 2001 From: Jeroen Noten Date: Thu, 12 Aug 2021 11:30:40 +0200 Subject: [PATCH 109/277] Add support for assertObjectHasAttribute --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 3 +++ tests/Type/PHPUnit/data/assert-function.php | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 8a56f324..2cd354cf 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -255,6 +255,9 @@ private static function getExpressionResolvers(): array 'ArrayHasKey' => function (Scope $scope, Arg $key, Arg $array): FuncCall { return new \PhpParser\Node\Expr\FuncCall(new Name('array_key_exists'), [$key, $array]); }, + 'ObjectHasAttribute' => function (Scope $scope, Arg $property, Arg $object): FuncCall { + return new \PhpParser\Node\Expr\FuncCall(new Name('property_exists'), [$object, $property]); + }, ]; } diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index f31fb378..ebfeb93a 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -4,8 +4,8 @@ use function PHPStan\Testing\assertType; use function PHPUnit\Framework\assertArrayHasKey; -use function PHPUnit\Framework\assertArrayNotHasKey; use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertObjectHasAttribute; class Foo { @@ -30,4 +30,10 @@ public function arrayHasStringKey(array $a): void assertType("array&hasOffset('key')", $a); } + public function objectHasAttribute(object $a): void + { + assertObjectHasAttribute('property', $a); + assertType("object&hasProperty(property)", $a); + } + } From 6c0e48e98f082e94be11bca4db64489194c66b06 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 12 Sep 2021 22:25:39 +0200 Subject: [PATCH 110/277] Open 1.0-dev --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 24d74cc0..ba598943 100644 --- a/composer.json +++ b/composer.json @@ -7,14 +7,14 @@ ], "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.92" + "phpstan/phpstan": "^1.0" }, "conflict": { "phpunit/phpunit": "<7.0" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^0.12.6", + "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5" }, "config": { @@ -25,7 +25,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.12-dev" + "dev-master": "1.0-dev" }, "phpstan": { "includes": [ From a316f5d59f620ef06f888f6fc95fead9d38e0377 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 12 Sep 2021 23:47:21 +0200 Subject: [PATCH 111/277] Enforce calling parent::setUp() and parent::tearDown() --- rules.neon | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/rules.neon b/rules.neon index d7c81ee1..5be69279 100644 --- a/rules.neon +++ b/rules.neon @@ -1,13 +1,6 @@ -conditionalTags: - PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% - -services: - - - class: PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule - rules: - PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule - PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule - PHPStan\Rules\PHPUnit\AssertSameWithCountRule - PHPStan\Rules\PHPUnit\MockMethodCallRule + - PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule From 88557fc8117c6cc2408eb0488372bd1659dbfc88 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 20 Sep 2021 17:54:35 +0200 Subject: [PATCH 112/277] Improve compatibility with PHPStan 1.0 --- phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 9c4be9d3..d71fc5ab 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,7 +5,7 @@ includes: - phar://phpstan.phar/conf/bleedingEdge.neon parameters: - excludes_analyse: + excludePaths: - tests/*/data/* services: From 46205241f54d7555b2d7ca82574faa9a54e32fcc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 20 Sep 2021 17:55:22 +0200 Subject: [PATCH 113/277] Improve compatibility with PHPParser 4.13.0 --- src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 4 ++-- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 4 ++-- src/Rules/PHPUnit/AssertSameWithCountRule.php | 6 +++--- src/Rules/PHPUnit/MockMethodCallRule.php | 4 ++-- .../Assert/AssertFunctionTypeSpecifyingExtension.php | 4 ++-- .../PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php | 4 ++-- .../Assert/AssertStaticMethodTypeSpecifyingExtension.php | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 82b7f864..97121853 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -26,14 +26,14 @@ public function processNode(Node $node, Scope $scope): array /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ $node = $node; - if (count($node->args) < 2) { + if (count($node->getArgs()) < 2) { return []; } if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { return []; } - $leftType = $scope->getType($node->args[0]->value); + $leftType = $scope->getType($node->getArgs()[0]->value); if (!$leftType instanceof ConstantBooleanType) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index b4c17325..9337cc83 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -26,14 +26,14 @@ public function processNode(Node $node, Scope $scope): array /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ $node = $node; - if (count($node->args) < 2) { + if (count($node->getArgs()) < 2) { return []; } if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { return []; } - $leftType = $scope->getType($node->args[0]->value); + $leftType = $scope->getType($node->getArgs()[0]->value); if ($leftType instanceof NullType) { return [ diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 6e21f537..3777b3fe 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -26,14 +26,14 @@ public function processNode(Node $node, Scope $scope): array /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ $node = $node; - if (count($node->args) < 2) { + if (count($node->getArgs()) < 2) { return []; } if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { return []; } - $right = $node->args[1]->value; + $right = $node->getArgs()[1]->value; if ( $right instanceof Node\Expr\FuncCall @@ -49,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array $right instanceof Node\Expr\MethodCall && $right->name instanceof Node\Identifier && strtolower($right->name->toString()) === 'count' - && count($right->args) === 0 + && count($right->getArgs()) === 0 ) { $type = $scope->getType($right->var); diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index d8bcd8a6..d7551dc9 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -31,11 +31,11 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (count($node->args) < 1) { + if (count($node->getArgs()) < 1) { return []; } - $argType = $scope->getType($node->args[0]->value); + $argType = $scope->getType($node->getArgs()[0]->value); if (!($argType instanceof ConstantStringType)) { return []; } diff --git a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php index d4ec8fb1..e20e13d1 100644 --- a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php @@ -30,7 +30,7 @@ public function isFunctionSupported( { return AssertTypeSpecifyingExtensionHelper::isSupported( $this->trimName($functionReflection->getName()), - $node->args + $node->getArgs() ); } @@ -45,7 +45,7 @@ public function specifyTypes( $this->typeSpecifier, $scope, $this->trimName($functionReflection->getName()), - $node->args + $node->getArgs() ); } diff --git a/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php index 243a7d4f..0fb0a3e7 100644 --- a/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php @@ -35,7 +35,7 @@ public function isMethodSupported( { return AssertTypeSpecifyingExtensionHelper::isSupported( $methodReflection->getName(), - $node->args + $node->getArgs() ); } @@ -50,7 +50,7 @@ public function specifyTypes( $this->typeSpecifier, $scope, $functionReflection->getName(), - $node->args + $node->getArgs() ); } diff --git a/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php index f33ee7a5..54da4457 100644 --- a/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php @@ -35,7 +35,7 @@ public function isStaticMethodSupported( { return AssertTypeSpecifyingExtensionHelper::isSupported( $methodReflection->getName(), - $node->args + $node->getArgs() ); } @@ -50,7 +50,7 @@ public function specifyTypes( $this->typeSpecifier, $scope, $functionReflection->getName(), - $node->args + $node->getArgs() ); } From 234cb5563905c448083dcd9d315ed882494798ab Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 20 Sep 2021 17:58:46 +0200 Subject: [PATCH 114/277] Constrain lowest nikic/php-parser to 4.13.0 --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index ba598943..8c3630b7 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "phpunit/phpunit": "<7.0" }, "require-dev": { + "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5" From 5d501422a47e99f288a1eb07e1de141df0b1eab4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 23 Sep 2021 10:37:07 +0200 Subject: [PATCH 115/277] Improve compatibility with PHPStan 1.0 --- src/Rules/PHPUnit/ShouldCallParentMethodsRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index 3d9c13ca..de8a778e 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -35,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array $parentClass = $scope->getClassReflection()->getParentClass(); - if ($parentClass === false) { + if ($parentClass === null) { return []; } if (!$parentClass->hasNativeMethod($methodName)) { From 54ed35b7d304cf4a30b1fe9bc0e61f900c4f0f45 Mon Sep 17 00:00:00 2001 From: Nicolas Reynis Date: Fri, 1 Oct 2021 22:24:26 +0200 Subject: [PATCH 116/277] Add type specifying support for classes extending Assert --- .../AssertMethodTypeSpecifyingExtension.php | 2 +- ...ssertMethodTypeSpecifyingExtensionTest.php | 36 +++++++++++++++++++ tests/Type/PHPUnit/data/assert-method.php | 17 +++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php create mode 100644 tests/Type/PHPUnit/data/assert-method.php diff --git a/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php index 0fb0a3e7..6307f244 100644 --- a/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php @@ -24,7 +24,7 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function getClass(): string { - return 'PHPUnit\Framework\TestCase'; + return 'PHPUnit\Framework\Assert'; } public function isMethodSupported( diff --git a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php new file mode 100644 index 00000000..6f6a1fc5 --- /dev/null +++ b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/assert-method.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param string $assertType + * @param string $file + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/../../../extension.neon']; + } + +} diff --git a/tests/Type/PHPUnit/data/assert-method.php b/tests/Type/PHPUnit/data/assert-method.php new file mode 100644 index 00000000..2f791ed7 --- /dev/null +++ b/tests/Type/PHPUnit/data/assert-method.php @@ -0,0 +1,17 @@ +assertNotNull($s); + assertType('string', $s); + } + +} From 9eb88c9f689003a8a2a5ae9e010338ee94dc39b3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 14 Oct 2021 10:03:54 +0200 Subject: [PATCH 117/277] Improve compatibility with PHPStan 1.0 --- .../Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 072603e1..dd96118a 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -36,7 +36,7 @@ public function testRule(): void 13, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\', \'b\') and array(1, 2) will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array{\'a\', \'b\'} and array{1, 2} will always evaluate to false.', 14, ], [ @@ -56,7 +56,7 @@ public function testRule(): void 47, ], [ - 'Call to method PHPUnit\Framework\Assert::assertSame() with array(\'a\', 2, 3.0) and array(\'a\', 1) will always evaluate to false.', + 'Call to method PHPUnit\Framework\Assert::assertSame() with array{\'a\', 2, 3.0} and array{\'a\', 1} will always evaluate to false.', 52, ], ]); From 320403ca77cf18768dc3afda2c9230de44e0dbbc Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 16 Dec 2021 19:32:44 +0100 Subject: [PATCH 118/277] Composer > Remove `--no-suggest` You are using the deprecated option "--no-suggest". It has no effect and will break in Composer 3. --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59b79ed7..85d9184d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: run: "composer validate" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Downgrade PHPUnit" if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' @@ -65,7 +65,7 @@ jobs: run: "composer validate" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Lint" run: "make lint" @@ -103,11 +103,11 @@ jobs: - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} - run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + run: "composer update --prefer-lowest --no-interaction --no-progress" - name: "Install highest dependencies" if: ${{ matrix.dependencies == 'highest' }} - run: "composer update --no-interaction --no-progress --no-suggest" + run: "composer update --no-interaction --no-progress" - name: "Downgrade PHPUnit" if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' @@ -148,11 +148,11 @@ jobs: - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} - run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + run: "composer update --prefer-lowest --no-interaction --no-progress" - name: "Install highest dependencies" if: ${{ matrix.dependencies == 'highest' }} - run: "composer update --no-interaction --no-progress --no-suggest" + run: "composer update --no-interaction --no-progress" - name: "Downgrade PHPUnit" if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' From 590a98b0658476594d5b4ec5c5bc60ce2963e8be Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 6 Jan 2022 13:41:47 +0100 Subject: [PATCH 119/277] Allow Composer plugins --- build-cs/composer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build-cs/composer.json b/build-cs/composer.json index ed7744e1..cc6a4983 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -3,5 +3,10 @@ "consistence-community/coding-standard": "^3.10", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "slevomat/coding-standard": "^6.4" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } From c9ab28bb6537163f5c4dc1ec6fe791977d321707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Sun, 16 Jan 2022 09:40:34 +0100 Subject: [PATCH 120/277] Tweet release action --- .github/workflows/release-tweet.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/release-tweet.yml diff --git a/.github/workflows/release-tweet.yml b/.github/workflows/release-tweet.yml new file mode 100644 index 00000000..09b39ded --- /dev/null +++ b/.github/workflows/release-tweet.yml @@ -0,0 +1,24 @@ +name: Tweet release + +# More triggers +# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release +on: + release: + types: [published] + +jobs: + tweet: + runs-on: ubuntu-latest + steps: + - uses: Eomm/why-don-t-you-tweet@v1 + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + tweet-message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" + env: + # Get your tokens from https://developer.twitter.com/apps + TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} + TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} From 59253fc2d4fecb246cf0657d9031a1d1ff1652e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Sun, 16 Jan 2022 11:53:50 +0100 Subject: [PATCH 121/277] Update release.yml --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 225470a6..5ed11761 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,13 +20,13 @@ jobs: id: changelog uses: metcalfc/changelog-generator@v1.0.0 with: - myToken: ${{ secrets.GITHUB_TOKEN }} + myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Create release" id: create-release uses: actions/create-release@v1 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} From bb6e2e2d1d7f193a7d87a35d9701f43bfb0cfbde Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 30 Jan 2022 18:04:43 +0100 Subject: [PATCH 122/277] Update phpunit.xml --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 8d53d3fd..8f71615a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,7 +10,7 @@ beStrictAboutTodoAnnotatedTests="true" failOnRisky="true" failOnWarning="true" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xml" > From fda8ef057ad5ec671bba3213c4eb95b0d3677482 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 1 Feb 2022 21:48:31 +0000 Subject: [PATCH 123/277] Add renovate.json --- renovate.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..f45d8f11 --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} From f59dda742b9a98e985de46394b9057de179c7b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 1 Feb 2022 23:15:27 +0100 Subject: [PATCH 124/277] Update and rename renovate.json to .github/renovate.json --- .github/renovate.json | 23 +++++++++++++++++++++++ renovate.json | 5 ----- 2 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 .github/renovate.json delete mode 100644 renovate.json diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..597ad920 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,23 @@ +{ + "extends": [ + "config:base" + ], + "rangeStrategy": "update-lockfile", + "packageRules": [ + { + "matchPaths": ["+(composer.json)"], + "enabled": true, + "groupName": "root-composer" + }, + { + "matchPaths": ["build-cs/**"], + "enabled": true, + "groupName": "build-cs" + }, + { + "matchPaths": [".github/**"], + "enabled": true, + "groupName": "github-actions" + } + ] +} diff --git a/renovate.json b/renovate.json deleted file mode 100644 index f45d8f11..00000000 --- a/renovate.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": [ - "config:base" - ] -} From 57e52d6d81a56591776e4b5cdb4180b961837180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 1 Feb 2022 23:16:03 +0100 Subject: [PATCH 125/277] Delete dependabot.yml --- .github/dependabot.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 15b77335..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: - - package-ecosystem: composer - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 - - package-ecosystem: composer - directory: "/build-cs" - schedule: - interval: monthly - open-pull-requests-limit: 10 - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 From ba2d5fadf783fd7cb587df86baac91c71217705c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 1 Feb 2022 22:15:48 +0000 Subject: [PATCH 126/277] Update metcalfc/changelog-generator action to v1.0.1 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ed11761..24a1d301 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v1.0.0 + uses: metcalfc/changelog-generator@v1.0.1 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From e0dcd5ffa833f8ce19679ba976879283bebc49d8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 2 Feb 2022 00:25:03 +0000 Subject: [PATCH 127/277] Update github-actions --- .github/workflows/lock-closed-issues.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 960c1ba5..9ea53272 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -8,7 +8,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2 + - uses: dessant/lock-threads@v3 with: github-token: ${{ github.token }} issue-lock-inactive-days: '31' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24a1d301..0ebed840 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v1.0.1 + uses: metcalfc/changelog-generator@v3.0.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 576db1b23b4a00e53a21be028d3cf67d01365ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Wed, 2 Feb 2022 15:39:40 +0100 Subject: [PATCH 128/277] Update lock-closed-issues.yml --- .github/workflows/lock-closed-issues.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 9ea53272..a05d4173 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -11,11 +11,11 @@ jobs: - uses: dessant/lock-threads@v3 with: github-token: ${{ github.token }} - issue-lock-inactive-days: '31' - issue-exclude-created-before: '' - issue-exclude-labels: '' - issue-lock-labels: '' - issue-lock-comment: > + issue-inactive-days: '31' + exclude-issue-created-before: '' + exclude-any-issue-labels: '' + add-issue-labels: '' + issue-comment: > This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. From da62e3c176751378feeb87c2b10182068ee8b276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Wed, 2 Feb 2022 16:38:02 +0100 Subject: [PATCH 129/277] Update renovate.json --- .github/renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 597ad920..b775cc18 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,6 +1,7 @@ { "extends": [ - "config:base" + "config:base", + "schedule:weekly" ], "rangeStrategy": "update-lockfile", "packageRules": [ From 15dc3e0d81b711c38d02fb33fe4482dfa570951c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 1 Feb 2022 22:15:54 +0000 Subject: [PATCH 130/277] Update dependency slevomat/coding-standard to v7 --- .gitignore | 2 +- build-cs/.gitignore | 1 - build-cs/composer.json | 2 +- build-cs/composer.lock | 327 ++++++++++++++++++ phpcs.xml | 98 ++++-- .../MockObjectTypeNodeResolverExtension.php | 7 +- src/Rules/PHPUnit/AssertRuleHelper.php | 2 + .../PHPUnit/AssertSameBooleanExpectedRule.php | 14 +- .../PHPUnit/AssertSameNullExpectedRule.php | 14 +- src/Rules/PHPUnit/AssertSameWithCountRule.php | 17 +- src/Rules/PHPUnit/MockMethodCallRule.php | 15 +- .../PHPUnit/ShouldCallParentMethodsRule.php | 10 +- .../AssertFunctionTypeSpecifyingExtension.php | 3 + .../AssertTypeSpecifyingExtensionHelper.php | 121 +++---- ...cationMockerDynamicReturnTypeExtension.php | 3 +- .../MockBuilderDynamicReturnTypeExtension.php | 4 +- .../MockObjectDynamicReturnTypeExtension.php | 8 +- .../AssertSameBooleanExpectedRuleTest.php | 5 +- ...AssertSameMethodDifferentTypesRuleTest.php | 5 +- .../AssertSameNullExpectedRuleTest.php | 5 +- ...SameStaticMethodDifferentTypesRuleTest.php | 5 +- .../PHPUnit/AssertSameWithCountRuleTest.php | 5 +- .../Rules/PHPUnit/MockMethodCallRuleTest.php | 5 +- .../ShouldCallParentMethodsRuleTest.php | 5 +- ...ertFunctionTypeSpecifyingExtensionTest.php | 3 +- ...ssertMethodTypeSpecifyingExtensionTest.php | 2 - 26 files changed, 554 insertions(+), 134 deletions(-) create mode 100644 build-cs/composer.lock diff --git a/.gitignore b/.gitignore index d6a83e59..2db21315 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /tests/tmp /vendor -composer.lock +/composer.lock .phpunit.result.cache diff --git a/build-cs/.gitignore b/build-cs/.gitignore index ff72e2d0..61ead866 100644 --- a/build-cs/.gitignore +++ b/build-cs/.gitignore @@ -1,2 +1 @@ -/composer.lock /vendor diff --git a/build-cs/composer.json b/build-cs/composer.json index cc6a4983..e3079710 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -2,7 +2,7 @@ "require-dev": { "consistence-community/coding-standard": "^3.10", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^6.4" + "slevomat/coding-standard": "^7.0" }, "config": { "allow-plugins": { diff --git a/build-cs/composer.lock b/build-cs/composer.lock new file mode 100644 index 00000000..70af78f9 --- /dev/null +++ b/build-cs/composer.lock @@ -0,0 +1,327 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4485bbedba7bcc71ace5f69dbb9b6c47", + "packages": [], + "packages-dev": [ + { + "name": "consistence-community/coding-standard", + "version": "3.11.1", + "source": { + "type": "git", + "url": "https://github.com/consistence-community/coding-standard.git", + "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/4632fead8c9ee8f50044fcbce9f66c797b34c0df", + "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "slevomat/coding-standard": "~7.0", + "squizlabs/php_codesniffer": "~3.6.0" + }, + "replace": { + "consistence/coding-standard": "3.10.*" + }, + "require-dev": { + "phing/phing": "2.16.4", + "php-parallel-lint/php-parallel-lint": "1.3.0", + "phpunit/phpunit": "9.5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Consistence\\": [ + "Consistence" + ] + }, + "classmap": [ + "Consistence" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Vašek Purchart", + "email": "me@vasekpurchart.cz", + "homepage": "http://vasekpurchart.cz" + } + ], + "description": "Consistence - Coding Standard - PHP Code Sniffer rules", + "keywords": [ + "Coding Standard", + "PHPCodeSniffer", + "codesniffer", + "coding", + "cs", + "phpcs", + "ruleset", + "sniffer", + "standard" + ], + "support": { + "issues": "https://github.com/consistence-community/coding-standard/issues", + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.1" + }, + "time": "2021-05-03T18:13:22+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.7.2", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2022-02-04T12:51:07+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.2.0" + }, + "time": "2021-09-16T20:46:02+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "7.0.18", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", + "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "php": "^7.1 || ^8.0", + "phpstan/phpdoc-parser": "^1.0.0", + "squizlabs/php_codesniffer": "^3.6.1" + }, + "require-dev": { + "phing/phing": "2.17.0", + "php-parallel-lint/php-parallel-lint": "1.3.1", + "phpstan/phpstan": "1.2.0", + "phpstan/phpstan-deprecation-rules": "1.0.0", + "phpstan/phpstan-phpunit": "1.0.0", + "phpstan/phpstan-strict-rules": "1.1.0", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.10" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/7.0.18" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2021-12-07T17:19:06+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.6.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2021-12-12T21:44:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.2.0" +} diff --git a/phpcs.xml b/phpcs.xml index ce6e811c..95032a6e 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,5 +1,6 @@ - + + @@ -8,58 +9,103 @@ src tests + - - + + - + + + + + - + - - - - - + + + + + 10 - - - + + 10 + - - - + + + + + 10 + + + - - + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - tests/tmp + + tests/*/data diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php index 955b8187..7225d6d3 100644 --- a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\PhpDoc\PHPUnit; +use PHPStan\Analyser\NameScope; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeNodeResolverAwareExtension; use PHPStan\PhpDoc\TypeNodeResolverExtension; @@ -9,7 +10,9 @@ use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeWithClassName; +use function array_key_exists; class MockObjectTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension { @@ -27,7 +30,7 @@ public function getCacheKey(): string return 'phpunit-v1'; } - public function resolve(TypeNode $typeNode, \PHPStan\Analyser\NameScope $nameScope): ?Type + public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type { if (!$typeNode instanceof UnionTypeNode) { return null; @@ -45,7 +48,7 @@ public function resolve(TypeNode $typeNode, \PHPStan\Analyser\NameScope $nameSco } if (array_key_exists($type->getClassName(), $mockClassNames)) { - $resultType = \PHPStan\Type\TypeCombinator::intersect(...$types); + $resultType = TypeCombinator::intersect(...$types); if ($resultType instanceof NeverType) { continue; } diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index e8edc506..96735379 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -5,6 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Type\ObjectType; +use function in_array; +use function strtolower; class AssertRuleHelper { diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 97121853..6be390ad 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -3,18 +3,24 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Type\Constant\ConstantBooleanType; +use function count; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> + * @implements Rule */ -class AssertSameBooleanExpectedRule implements \PHPStan\Rules\Rule +class AssertSameBooleanExpectedRule implements Rule { public function getNodeType(): string { - return \PhpParser\NodeAbstract::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array @@ -23,7 +29,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ + /** @var MethodCall|StaticCall $node */ $node = $node; if (count($node->getArgs()) < 2) { diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 9337cc83..8d2b2aad 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -3,18 +3,24 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Type\NullType; +use function count; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> + * @implements Rule */ -class AssertSameNullExpectedRule implements \PHPStan\Rules\Rule +class AssertSameNullExpectedRule implements Rule { public function getNodeType(): string { - return \PhpParser\NodeAbstract::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array @@ -23,7 +29,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ + /** @var MethodCall|StaticCall $node */ $node = $node; if (count($node->getArgs()) < 2) { diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 3777b3fe..1f1a3ab2 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -2,19 +2,26 @@ namespace PHPStan\Rules\PHPUnit; +use Countable; use PhpParser\Node; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Type\ObjectType; +use function count; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> + * @implements Rule */ -class AssertSameWithCountRule implements \PHPStan\Rules\Rule +class AssertSameWithCountRule implements Rule { public function getNodeType(): string { - return \PhpParser\NodeAbstract::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array @@ -23,7 +30,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ + /** @var MethodCall|StaticCall $node */ $node = $node; if (count($node->getArgs()) < 2) { @@ -53,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array ) { $type = $scope->getType($right->var); - if ((new ObjectType(\Countable::class))->isSuperTypeOf($type)->yes()) { + if ((new ObjectType(Countable::class))->isSuperTypeOf($type)->yes()) { return [ 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).', ]; diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index d7551dc9..0007aa97 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -3,18 +3,25 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; +use function array_filter; +use function count; +use function implode; +use function in_array; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall> + * @implements Rule */ -class MockMethodCallRule implements \PHPStan\Rules\Rule +class MockMethodCallRule implements Rule { public function getNodeType(): string @@ -48,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array && in_array(MockObject::class, $type->getReferencedClasses(), true) && !$type->hasMethod($method)->yes() ) { - $mockClass = array_filter($type->getReferencedClasses(), function (string $class): bool { + $mockClass = array_filter($type->getReferencedClasses(), static function (string $class): bool { return $class !== MockObject::class; }); @@ -56,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - \implode('&', $mockClass) + implode('&', $mockClass) ), ]; } diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index de8a778e..01414061 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -5,13 +5,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPUnit\Framework\TestCase; +use function in_array; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class ShouldCallParentMethodsRule implements \PHPStan\Rules\Rule +class ShouldCallParentMethodsRule implements Rule { public function getNodeType(): string @@ -62,9 +66,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Stmt[]|null $stmts - * @param string $methodName * - * @return bool */ private function hasParentClassCall(?array $stmts, string $methodName): bool { diff --git a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php index e20e13d1..31805a3f 100644 --- a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php @@ -10,6 +10,9 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function strlen; +use function strpos; +use function substr; class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 2cd354cf..36203e65 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -2,8 +2,12 @@ namespace PHPStan\Type\PHPUnit\Assert; +use Closure; use PhpParser\Node\Arg; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; +use PhpParser\Node\Expr\BooleanNot; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Name; @@ -12,17 +16,21 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Type\Constant\ConstantStringType; +use ReflectionObject; +use function array_key_exists; +use function count; +use function strlen; +use function strpos; +use function substr; class AssertTypeSpecifyingExtensionHelper { - /** @var \Closure[] */ + /** @var Closure[] */ private static $resolvers; /** - * @param string $name - * @param \PhpParser\Node\Arg[] $args - * @return bool + * @param Arg[] $args */ public static function isSupported( string $name, @@ -37,9 +45,9 @@ public static function isSupported( } $resolver = $resolvers[$trimmedName]; - $resolverReflection = new \ReflectionObject($resolver); + $resolverReflection = new ReflectionObject($resolver); - return count($args) >= (count($resolverReflection->getMethod('__invoke')->getParameters()) - 1); + return count($args) >= count($resolverReflection->getMethod('__invoke')->getParameters()) - 1; } private static function trimName(string $name): string @@ -62,11 +70,7 @@ private static function trimName(string $name): string } /** - * @param TypeSpecifier $typeSpecifier - * @param Scope $scope - * @param string $name - * @param \PhpParser\Node\Arg[] $args $args - * @return SpecifiedTypes + * @param Arg[] $args $args */ public static function specifyTypes( TypeSpecifier $typeSpecifier, @@ -87,16 +91,13 @@ public static function specifyTypes( } /** - * @param Scope $scope - * @param string $name - * @param \PhpParser\Node\Arg[] $args - * @return \PhpParser\Node\Expr|null + * @param Arg[] $args */ private static function createExpression( Scope $scope, string $name, array $args - ): ?\PhpParser\Node\Expr + ): ?Expr { $trimmedName = self::trimName($name); $resolvers = self::getExpressionResolvers(); @@ -107,88 +108,88 @@ private static function createExpression( } if (strpos($name, 'Not') !== false) { - $expression = new \PhpParser\Node\Expr\BooleanNot($expression); + $expression = new BooleanNot($expression); } return $expression; } /** - * @return \Closure[] + * @return Closure[] */ private static function getExpressionResolvers(): array { if (self::$resolvers === null) { self::$resolvers = [ - 'InstanceOf' => function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { + 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { $classType = $scope->getType($class->value); if (!$classType instanceof ConstantStringType) { return null; } - return new \PhpParser\Node\Expr\Instanceof_( + return new Instanceof_( $object->value, - new \PhpParser\Node\Name($classType->getValue()) + new Name($classType->getValue()) ); }, - 'Same' => function (Scope $scope, Arg $expected, Arg $actual): Identical { - return new \PhpParser\Node\Expr\BinaryOp\Identical( + 'Same' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { + return new Identical( $expected->value, $actual->value ); }, - 'True' => function (Scope $scope, Arg $actual): Identical { - return new \PhpParser\Node\Expr\BinaryOp\Identical( + 'True' => static function (Scope $scope, Arg $actual): Identical { + return new Identical( $actual->value, - new \PhpParser\Node\Expr\ConstFetch(new Name('true')) + new ConstFetch(new Name('true')) ); }, - 'False' => function (Scope $scope, Arg $actual): Identical { - return new \PhpParser\Node\Expr\BinaryOp\Identical( + 'False' => static function (Scope $scope, Arg $actual): Identical { + return new Identical( $actual->value, - new \PhpParser\Node\Expr\ConstFetch(new Name('false')) + new ConstFetch(new Name('false')) ); }, - 'Null' => function (Scope $scope, Arg $actual): Identical { - return new \PhpParser\Node\Expr\BinaryOp\Identical( + 'Null' => static function (Scope $scope, Arg $actual): Identical { + return new Identical( $actual->value, - new \PhpParser\Node\Expr\ConstFetch(new Name('null')) + new ConstFetch(new Name('null')) ); }, - 'IsArray' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_array'), [$actual]); + 'IsArray' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_array'), [$actual]); }, - 'IsBool' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_bool'), [$actual]); + 'IsBool' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_bool'), [$actual]); }, - 'IsCallable' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_callable'), [$actual]); + 'IsCallable' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_callable'), [$actual]); }, - 'IsFloat' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_float'), [$actual]); + 'IsFloat' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_float'), [$actual]); }, - 'IsInt' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_int'), [$actual]); + 'IsInt' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_int'), [$actual]); }, - 'IsIterable' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_iterable'), [$actual]); + 'IsIterable' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_iterable'), [$actual]); }, - 'IsNumeric' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_numeric'), [$actual]); + 'IsNumeric' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_numeric'), [$actual]); }, - 'IsObject' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_object'), [$actual]); + 'IsObject' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_object'), [$actual]); }, - 'IsResource' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_resource'), [$actual]); + 'IsResource' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_resource'), [$actual]); }, - 'IsString' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_string'), [$actual]); + 'IsString' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_string'), [$actual]); }, - 'IsScalar' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_scalar'), [$actual]); + 'IsScalar' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_scalar'), [$actual]); }, - 'InternalType' => function (Scope $scope, Arg $type, Arg $value): ?FuncCall { + 'InternalType' => static function (Scope $scope, Arg $type, Arg $value): ?FuncCall { $typeType = $scope->getType($type->value); if (!$typeType instanceof ConstantStringType) { return null; @@ -245,18 +246,18 @@ private static function getExpressionResolvers(): array return null; } - return new \PhpParser\Node\Expr\FuncCall( + return new FuncCall( new Name($functionName), [ $value, ] ); }, - 'ArrayHasKey' => function (Scope $scope, Arg $key, Arg $array): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('array_key_exists'), [$key, $array]); + 'ArrayHasKey' => static function (Scope $scope, Arg $key, Arg $array): FuncCall { + return new FuncCall(new Name('array_key_exists'), [$key, $array]); }, - 'ObjectHasAttribute' => function (Scope $scope, Arg $property, Arg $object): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('property_exists'), [$object, $property]); + 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { + return new FuncCall(new Name('property_exists'), [$object, $property]); }, ]; } diff --git a/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php b/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php index 51e22221..44764f62 100644 --- a/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php @@ -5,10 +5,11 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; -class InvocationMockerDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +class InvocationMockerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php index d6ae40d3..5390b11c 100644 --- a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php @@ -5,10 +5,12 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; use PHPUnit\Framework\MockObject\MockBuilder; +use function in_array; -class MockBuilderDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +class MockBuilderDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php index 06e69b3d..cb0b3a17 100644 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; @@ -12,8 +13,11 @@ use PHPStan\Type\TypeWithClassName; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; +use function array_filter; +use function array_values; +use function count; -class MockObjectDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +class MockObjectDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string @@ -33,7 +37,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return new ObjectType(InvocationMocker::class); } - $mockClasses = array_values(array_filter($type->getTypes(), function (Type $type): bool { + $mockClasses = array_values(array_filter($type->getTypes(), static function (Type $type): bool { return !$type instanceof TypeWithClassName || $type->getClassName() !== MockObject::class; })); diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php index a96aa0a1..916884c7 100644 --- a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameBooleanExpectedRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameBooleanExpectedRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index dd96118a..774223f0 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -4,11 +4,12 @@ use PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameMethodDifferentTypesRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php index 009a96d0..c2a2242f 100644 --- a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameNullExpectedRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameNullExpectedRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index e55d3e22..cc65b273 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -4,11 +4,12 @@ use PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameStaticMethodDifferentTypesRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php index 9a0caf91..32f564d6 100644 --- a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameWithCountRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameWithCountRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php index cd426781..605daae1 100644 --- a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MockMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +class MockMethodCallRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php index 3f8d6e2d..b378c67f 100644 --- a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php +++ b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ShouldCallParentMethodsRuleTest extends \PHPStan\Testing\RuleTestCase +class ShouldCallParentMethodsRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php index c32b9d0a..f2a254d7 100644 --- a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\PHPUnit; use PHPStan\Testing\TypeInferenceTestCase; +use function function_exists; class AssertFunctionTypeSpecifyingExtensionTest extends TypeInferenceTestCase { @@ -19,8 +20,6 @@ public function dataFileAsserts(): iterable /** * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function testFileAsserts( diff --git a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php index 6f6a1fc5..e1841e0b 100644 --- a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php @@ -15,8 +15,6 @@ public function dataFileAsserts(): iterable /** * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function testFileAsserts( From 79201b0d5269410fb6b6a0bc568f180ca9dec2ac Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 7 Mar 2022 02:00:33 +0000 Subject: [PATCH 131/277] Update actions/checkout action to v3 --- .github/workflows/build.yml | 8 ++++---- .github/workflows/release.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85d9184d..49be33fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -53,7 +53,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -93,7 +93,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -136,7 +136,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ebed840..5fed0458 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: Generate changelog id: changelog From 1ca3c9814a478c2cc04111539bf72fcc48b00926 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 7 Mar 2022 02:00:29 +0000 Subject: [PATCH 132/277] Update dependency slevomat/coding-standard to v7.0.19 --- build-cs/composer.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 70af78f9..96177edd 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -200,32 +200,32 @@ }, { "name": "slevomat/coding-standard", - "version": "7.0.18", + "version": "7.0.19", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc" + "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", - "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", + "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", "php": "^7.1 || ^8.0", "phpstan/phpdoc-parser": "^1.0.0", - "squizlabs/php_codesniffer": "^3.6.1" + "squizlabs/php_codesniffer": "^3.6.2" }, "require-dev": { - "phing/phing": "2.17.0", - "php-parallel-lint/php-parallel-lint": "1.3.1", - "phpstan/phpstan": "1.2.0", + "phing/phing": "2.17.2", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.4.6", "phpstan/phpstan-deprecation-rules": "1.0.0", "phpstan/phpstan-phpunit": "1.0.0", "phpstan/phpstan-strict-rules": "1.1.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.10" + "phpunit/phpunit": "7.5.20|8.5.21|9.5.16" }, "type": "phpcodesniffer-standard", "extra": { @@ -245,7 +245,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.0.18" + "source": "https://github.com/slevomat/coding-standard/tree/7.0.19" }, "funding": [ { @@ -257,7 +257,7 @@ "type": "tidelift" } ], - "time": "2021-12-07T17:19:06+00:00" + "time": "2022-03-01T18:01:41+00:00" }, { "name": "squizlabs/php_codesniffer", From b5cc290488e034ddfa998874586cebaecf9626cd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 9 Mar 2022 21:00:28 +0100 Subject: [PATCH 133/277] Require PHPStan 1.4.9 --- composer.json | 2 +- .../PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8c3630b7..2368c93e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "phpstan/phpstan": "^1.4.9" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 774223f0..87e5a3af 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -48,6 +48,10 @@ public function testRule(): void 'Call to method PHPUnit\Framework\Assert::assertSame() with 1 and 1 will always evaluate to true.', 44, ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() with array{\'a\'} and array{\'a\', \'b\'} will always evaluate to false.', + 45, + ], [ 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and \'1\' will always evaluate to true.', 46, @@ -56,6 +60,10 @@ public function testRule(): void 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and \'2\' will always evaluate to false.', 47, ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() with array{\'a\'} and array{\'a\', 1} will always evaluate to false.', + 51, + ], [ 'Call to method PHPUnit\Framework\Assert::assertSame() with array{\'a\', 2, 3.0} and array{\'a\', 1} will always evaluate to false.', 52, From 0fcf8217c3db211dd4a231b019cb6a1a2b8feabb Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 10 Mar 2022 09:24:10 +0100 Subject: [PATCH 134/277] Add assertSame test case with static method --- tests/Rules/PHPUnit/data/assert-same.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Rules/PHPUnit/data/assert-same.php b/tests/Rules/PHPUnit/data/assert-same.php index 48b81e4b..42115b75 100644 --- a/tests/Rules/PHPUnit/data/assert-same.php +++ b/tests/Rules/PHPUnit/data/assert-same.php @@ -65,4 +65,17 @@ public function testOther() $foo->assertSame(); } + public function testStaticMethodReturnWithSameTypeIsNotReported() + { + $this->assertSame(self::createSomething('foo'), self::createSomething('foo')); + } + + /** + * @return object + */ + private static function createSomething(string $what) + { + return new \stdClass(); + } + } From 886dab73be32ade686172cb32add7045861a9a4e Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 10 Mar 2022 09:29:14 +0100 Subject: [PATCH 135/277] Add test case for assertNotSame as well --- tests/Rules/PHPUnit/data/assert-same.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Rules/PHPUnit/data/assert-same.php b/tests/Rules/PHPUnit/data/assert-same.php index 42115b75..41384e5c 100644 --- a/tests/Rules/PHPUnit/data/assert-same.php +++ b/tests/Rules/PHPUnit/data/assert-same.php @@ -68,6 +68,7 @@ public function testOther() public function testStaticMethodReturnWithSameTypeIsNotReported() { $this->assertSame(self::createSomething('foo'), self::createSomething('foo')); + $this->assertNotSame(self::createSomething('bar'), self::createSomething('bar')); } /** From 8615099a0207f72c302b98b847c3bdc7c75ffe44 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 28 Mar 2022 00:22:05 +0000 Subject: [PATCH 136/277] Update dependency slevomat/coding-standard to v7.0.20 --- build-cs/composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 96177edd..16f621d4 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -200,16 +200,16 @@ }, { "name": "slevomat/coding-standard", - "version": "7.0.19", + "version": "7.0.20", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37" + "reference": "cbfadfe34c2c29473bf1e891306b3950b3b4350b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", - "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cbfadfe34c2c29473bf1e891306b3950b3b4350b", + "reference": "cbfadfe34c2c29473bf1e891306b3950b3b4350b", "shasum": "" }, "require": { @@ -221,11 +221,11 @@ "require-dev": { "phing/phing": "2.17.2", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.6", + "phpstan/phpstan": "1.4.10|1.5.0", "phpstan/phpstan-deprecation-rules": "1.0.0", "phpstan/phpstan-phpunit": "1.0.0", "phpstan/phpstan-strict-rules": "1.1.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.16" + "phpunit/phpunit": "7.5.20|8.5.21|9.5.19" }, "type": "phpcodesniffer-standard", "extra": { @@ -245,7 +245,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.0.19" + "source": "https://github.com/slevomat/coding-standard/tree/7.0.20" }, "funding": [ { @@ -257,7 +257,7 @@ "type": "tidelift" } ], - "time": "2022-03-01T18:01:41+00:00" + "time": "2022-03-25T09:43:20+00:00" }, { "name": "squizlabs/php_codesniffer", From 98bb1842ca8413193fa4ae2cef6f9cc7e8892fc8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Mar 2022 11:15:51 +0200 Subject: [PATCH 137/277] Drop support for PHP 7.2, require PHPStan 1.5.0 --- .github/workflows/build.yml | 9 +++------ composer.json | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49be33fa..8e19c3ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,6 @@ jobs: strategy: matrix: php-version: - - "7.1" - "7.2" - "7.3" - "7.4" @@ -40,7 +39,7 @@ jobs: run: "composer install --no-interaction --no-progress" - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + if: matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - name: "Lint" @@ -81,7 +80,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.1" - "7.2" - "7.3" - "7.4" @@ -110,7 +108,7 @@ jobs: run: "composer update --no-interaction --no-progress" - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + if: matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - name: "Tests" @@ -124,7 +122,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.1" - "7.2" - "7.3" - "7.4" @@ -155,7 +152,7 @@ jobs: run: "composer update --no-interaction --no-progress" - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + if: matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - name: "PHPStan" diff --git a/composer.json b/composer.json index 2368c93e..febce308 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,8 @@ "MIT" ], "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.4.9" + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.5.0" }, "conflict": { "phpunit/phpunit": "<7.0" From a092e4961a1a1e1509cc658e2ad6c0f202655343 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Mar 2022 11:17:23 +0200 Subject: [PATCH 138/277] Update workflow --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8e19c3ed..82332bb6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "master" + - "1.1.x" jobs: lint: From f26b2a2e7ae1ce2b94288f2d3096fcf09b93682e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Mar 2022 11:17:36 +0200 Subject: [PATCH 139/277] Drop alias --- composer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/composer.json b/composer.json index febce308..2f67ed5a 100644 --- a/composer.json +++ b/composer.json @@ -25,9 +25,6 @@ "sort-packages": true }, "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon", From 09133ce914f1388a8bb8c7f8573aaa3723cff52a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 27 Mar 2022 10:04:28 +0200 Subject: [PATCH 140/277] CreateStub returns a Stub --- extension.neon | 1 + .../MockObjectTypeNodeResolverExtension.php | 1 + src/Rules/PHPUnit/MockMethodCallRule.php | 8 ++++++-- stubs/Stub.stub | 8 ++++++++ stubs/TestCase.stub | 3 ++- tests/Rules/PHPUnit/MockMethodCallRuleTest.php | 14 ++++++++++++-- tests/Rules/PHPUnit/data/mock-method-call.php | 10 ++++++++++ 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 stubs/Stub.stub diff --git a/extension.neon b/extension.neon index 10254483..93d3a9ed 100644 --- a/extension.neon +++ b/extension.neon @@ -12,6 +12,7 @@ parameters: - stubs/InvocationMocker.stub - stubs/MockBuilder.stub - stubs/MockObject.stub + - stubs/Stub.stub - stubs/TestCase.stub exceptions: uncheckedExceptionRegexes: diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php index 7225d6d3..4d463799 100644 --- a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -39,6 +39,7 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type static $mockClassNames = [ 'PHPUnit_Framework_MockObject_MockObject' => true, 'PHPUnit\Framework\MockObject\MockObject' => true, + 'PHPUnit\Framework\MockObject\Stub' => true, ]; $types = $this->typeNodeResolver->resolveMultiple($typeNode->types, $nameScope); diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index 0007aa97..ae554e1f 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -12,6 +12,7 @@ use PHPStan\Type\ObjectType; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Stub; use function array_filter; use function count; use function implode; @@ -52,11 +53,14 @@ public function processNode(Node $node, Scope $scope): array if ( $type instanceof IntersectionType - && in_array(MockObject::class, $type->getReferencedClasses(), true) + && ( + in_array(MockObject::class, $type->getReferencedClasses(), true) + || in_array(Stub::class, $type->getReferencedClasses(), true) + ) && !$type->hasMethod($method)->yes() ) { $mockClass = array_filter($type->getReferencedClasses(), static function (string $class): bool { - return $class !== MockObject::class; + return $class !== MockObject::class && $class !== Stub::class; }); return [ diff --git a/stubs/Stub.stub b/stubs/Stub.stub new file mode 100644 index 00000000..62771cca --- /dev/null +++ b/stubs/Stub.stub @@ -0,0 +1,8 @@ + $originalClassName - * @phpstan-return MockObject&T + * @phpstan-return Stub&T */ public function createStub($originalClassName) {} diff --git a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php index 605daae1..c9c33e6f 100644 --- a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function interface_exists; /** * @extends RuleTestCase @@ -18,7 +19,7 @@ protected function getRule(): Rule public function testRule(): void { - $this->analyse([__DIR__ . '/data/mock-method-call.php'], [ + $expectedErrors = [ [ 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', 15, @@ -27,7 +28,16 @@ public function testRule(): void 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', 20, ], - ]); + ]; + + if (interface_exists('PHPUnit\Framework\MockObject\Builder\InvocationStubber')) { + $expectedErrors[] = [ + 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', + 36, + ]; + } + + $this->analyse([__DIR__ . '/data/mock-method-call.php'], $expectedErrors); } /** diff --git a/tests/Rules/PHPUnit/data/mock-method-call.php b/tests/Rules/PHPUnit/data/mock-method-call.php index cbe59429..bf0fd053 100644 --- a/tests/Rules/PHPUnit/data/mock-method-call.php +++ b/tests/Rules/PHPUnit/data/mock-method-call.php @@ -26,6 +26,16 @@ public function testWithAnotherObject() $bar->method('doBadThing'); } + public function testGoodMethodOnStub() + { + $this->createStub(Bar::class)->method('doThing'); + } + + public function testBadMethodOnStub() + { + $this->createStub(Bar::class)->method('doBadThing'); + } + } class Bar { From 2832aad142674a294cad11ecd5054abf7b6d54bd Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 4 Apr 2022 01:18:42 +0000 Subject: [PATCH 141/277] Update dependency slevomat/coding-standard to v7.1 --- build-cs/composer.lock | 43 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 16f621d4..5a10fffd 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -151,35 +151,30 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.2.0", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" + "reference": "4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d", + "reference": "4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", + "phpstan/phpstan": "^1.5", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { "psr-4": { "PHPStan\\PhpDocParser\\": [ @@ -194,36 +189,36 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.2.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.4.2" }, - "time": "2021-09-16T20:46:02+00:00" + "time": "2022-03-30T13:33:37+00:00" }, { "name": "slevomat/coding-standard", - "version": "7.0.20", + "version": "7.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "cbfadfe34c2c29473bf1e891306b3950b3b4350b" + "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cbfadfe34c2c29473bf1e891306b3950b3b4350b", - "reference": "cbfadfe34c2c29473bf1e891306b3950b3b4350b", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b521bd358b5f7a7d69e9637fd139e036d8adeb6f", + "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", - "php": "^7.1 || ^8.0", - "phpstan/phpdoc-parser": "^1.0.0", + "php": "^7.2 || ^8.0", + "phpstan/phpdoc-parser": "^1.4.1", "squizlabs/php_codesniffer": "^3.6.2" }, "require-dev": { "phing/phing": "2.17.2", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.5.0", + "phpstan/phpstan": "1.4.10|1.5.2", "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0", + "phpstan/phpstan-phpunit": "1.0.0|1.1.0", "phpstan/phpstan-strict-rules": "1.1.0", "phpunit/phpunit": "7.5.20|8.5.21|9.5.19" }, @@ -245,7 +240,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.0.20" + "source": "https://github.com/slevomat/coding-standard/tree/7.1" }, "funding": [ { @@ -257,7 +252,7 @@ "type": "tidelift" } ], - "time": "2022-03-25T09:43:20+00:00" + "time": "2022-03-29T12:44:16+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -323,5 +318,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } From 4a3c437c09075736285d1cabb5c75bf27ed0bc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=C5=A1ek=20Purchart?= Date: Wed, 20 Apr 2022 17:24:25 +0200 Subject: [PATCH 142/277] AssertSameBooleanExpectedRule, AssertSameNullExpectedRule - report only ConstFetch * Add more tests for AssertSameBooleanExpectedRule * Report only direct true|false for assertTrue()|asertFalse() * Add more tests for AssertSameNullExpectedRule * Report only direct null for assertNull() --- .../PHPUnit/AssertSameBooleanExpectedRule.php | 18 +++--- .../PHPUnit/AssertSameNullExpectedRule.php | 9 ++- .../AssertSameBooleanExpectedRuleTest.php | 10 ++-- .../AssertSameNullExpectedRuleTest.php | 4 +- .../data/assert-same-boolean-expected.php | 56 ++++++++++++++++++- .../data/assert-same-null-expected.php | 40 ++++++++++++- 6 files changed, 117 insertions(+), 20 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 6be390ad..d24d4904 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -3,12 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\Constant\ConstantBooleanType; use function count; use function strtolower; @@ -39,20 +39,24 @@ public function processNode(Node $node, Scope $scope): array return []; } - $leftType = $scope->getType($node->getArgs()[0]->value); - if (!$leftType instanceof ConstantBooleanType) { + $expectedArgumentValue = $node->getArgs()[0]->value; + if (!($expectedArgumentValue instanceof ConstFetch)) { return []; } - if ($leftType->getValue()) { + if ($expectedArgumentValue->name->toLowerString() === 'true') { return [ 'You should use assertTrue() instead of assertSame() when expecting "true"', ]; } - return [ - 'You should use assertFalse() instead of assertSame() when expecting "false"', - ]; + if ($expectedArgumentValue->name->toLowerString() === 'false') { + return [ + 'You should use assertFalse() instead of assertSame() when expecting "false"', + ]; + } + + return []; } } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 8d2b2aad..672f3496 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -3,12 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\NullType; use function count; use function strtolower; @@ -39,9 +39,12 @@ public function processNode(Node $node, Scope $scope): array return []; } - $leftType = $scope->getType($node->getArgs()[0]->value); + $expectedArgumentValue = $node->getArgs()[0]->value; + if (!($expectedArgumentValue instanceof ConstFetch)) { + return []; + } - if ($leftType instanceof NullType) { + if ($expectedArgumentValue->name->toLowerString() === 'null') { return [ 'You should use assertNull() instead of assertSame(null, $actual).', ]; diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php index 916884c7..1fe31df9 100644 --- a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -29,15 +29,15 @@ public function testRule(): void ], [ 'You should use assertTrue() instead of assertSame() when expecting "true"', - 14, + 26, ], [ - 'You should use assertFalse() instead of assertSame() when expecting "false"', - 17, + 'You should use assertTrue() instead of assertSame() when expecting "true"', + 74, ], [ - 'You should use assertTrue() instead of assertSame() when expecting "true"', - 26, + 'You should use assertFalse() instead of assertSame() when expecting "false"', + 75, ], ]); } diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php index c2a2242f..1e802dc0 100644 --- a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -25,11 +25,11 @@ public function testRule(): void ], [ 'You should use assertNull() instead of assertSame(null, $actual).', - 13, + 24, ], [ 'You should use assertNull() instead of assertSame(null, $actual).', - 24, + 60, ], ]); } diff --git a/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php index 1158a574..dccd2ceb 100644 --- a/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php +++ b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php @@ -11,10 +11,10 @@ public function testAssertSameWithBooleanAsExpected() $this->assertSame(false, 'a'); $truish = true; - $this->assertSame($truish, true); + $this->assertSame($truish, true); // using variable is OK $falsish = false; - $this->assertSame($falsish, false); + $this->assertSame($falsish, false); // using variable is OK /** @var bool $a */ $a = null; @@ -26,4 +26,56 @@ public function testAssertSameIsDetectedWithDirectAssertAccess() \PHPUnit\Framework\Assert::assertSame(true, 'foo'); } + public function testConstants(): void + { + \PHPUnit\Framework\Assert::assertSame(PHPSTAN_PHPUNIT_TRUE, 'foo'); + \PHPUnit\Framework\Assert::assertSame(PHPSTAN_PHPUNIT_FALSE, 'foo'); + } + + private const TRUE = true; + private const FALSE = false; + + public function testClassConstants(): void + { + \PHPUnit\Framework\Assert::assertSame(self::TRUE, 'foo'); + \PHPUnit\Framework\Assert::assertSame(self::FALSE, 'foo'); + } + + public function returnBool(): bool + { + return true; + } + + /** + * @return true + */ + public function returnTrue(): bool + { + return true; + } + + /** + * @return false + */ + public function returnFalse(): bool + { + return false; + } + + public function testMethodCalls(): void + { + \PHPUnit\Framework\Assert::assertSame($this->returnTrue(), 'foo'); + \PHPUnit\Framework\Assert::assertSame($this->returnFalse(), 'foo'); + \PHPUnit\Framework\Assert::assertSame($this->returnBool(), 'foo'); + } + + public function testNonLowercase(): void + { + \PHPUnit\Framework\Assert::assertSame(True, 'foo'); + \PHPUnit\Framework\Assert::assertSame(False, 'foo'); + } + } + +const PHPSTAN_PHPUNIT_TRUE = true; +const PHPSTAN_PHPUNIT_FALSE = false; diff --git a/tests/Rules/PHPUnit/data/assert-same-null-expected.php b/tests/Rules/PHPUnit/data/assert-same-null-expected.php index 8c7be339..fedc4c98 100644 --- a/tests/Rules/PHPUnit/data/assert-same-null-expected.php +++ b/tests/Rules/PHPUnit/data/assert-same-null-expected.php @@ -10,7 +10,7 @@ public function testAssertSameWithNullAsExpected() $this->assertSame(null, 'a'); $a = null; - $this->assertSame($a, 'b'); + $this->assertSame($a, 'b'); // using variable is OK $this->assertSame('a', 'b'); // OK @@ -24,4 +24,42 @@ public function testAssertSameIsDetectedWithDirectAssertAccess() \PHPUnit\Framework\Assert::assertSame(null, 'foo'); } + public function testConstant(): void + { + \PHPUnit\Framework\Assert::assertSame(PHPSTAN_PHPUNIT_NULL, 'foo'); + } + + private const NULL = null; + + public function testClassConstant(): void + { + \PHPUnit\Framework\Assert::assertSame(self::NULL, 'foo'); + } + + public function returnNullable(): ?string + { + + } + + /** + * @return null + */ + public function returnNull() + { + return null; + } + + public function testMethodCalls(): void + { + \PHPUnit\Framework\Assert::assertSame($this->returnNull(), 'foo'); + \PHPUnit\Framework\Assert::assertSame($this->returnNullable(), 'foo'); + } + + public function testNonLowercase(): void + { + \PHPUnit\Framework\Assert::assertSame(Null, 'foo'); + } + } + +const PHPSTAN_PHPUNIT_NULL = null; From e00da5f7e5c25cbfe6a39d69a8f6439b93307d98 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 9 May 2022 01:36:55 +0000 Subject: [PATCH 143/277] Update dependency slevomat/coding-standard to v7.2.0 --- build-cs/composer.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 5a10fffd..6e7fb818 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -151,16 +151,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.4.2", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d" + "reference": "981cc368a216c988e862a75e526b6076987d1b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d", - "reference": "4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/981cc368a216c988e862a75e526b6076987d1b50", + "reference": "981cc368a216c988e862a75e526b6076987d1b50", "shasum": "" }, "require": { @@ -189,38 +189,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.4.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.5.1" }, - "time": "2022-03-30T13:33:37+00:00" + "time": "2022-05-05T11:32:40+00:00" }, { "name": "slevomat/coding-standard", - "version": "7.1", + "version": "7.2.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f" + "reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b521bd358b5f7a7d69e9637fd139e036d8adeb6f", - "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b4f96a8beea515d2d89141b7b9ad72f526d84071", + "reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.4.1", + "phpstan/phpdoc-parser": "^1.5.1", "squizlabs/php_codesniffer": "^3.6.2" }, "require-dev": { - "phing/phing": "2.17.2", + "phing/phing": "2.17.3", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.5.2", + "phpstan/phpstan": "1.4.10|1.6.7", "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.1.0", - "phpstan/phpstan-strict-rules": "1.1.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.19" + "phpstan/phpstan-phpunit": "1.0.0|1.1.1", + "phpstan/phpstan-strict-rules": "1.2.3", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.20" }, "type": "phpcodesniffer-standard", "extra": { @@ -240,7 +240,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.1" + "source": "https://github.com/slevomat/coding-standard/tree/7.2.0" }, "funding": [ { @@ -252,7 +252,7 @@ "type": "tidelift" } ], - "time": "2022-03-29T12:44:16+00:00" + "time": "2022-05-06T10:58:42+00:00" }, { "name": "squizlabs/php_codesniffer", From 694fe403a66a02631851b840d5bd839a096e200c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 30 May 2022 02:04:17 +0000 Subject: [PATCH 144/277] Update dependency slevomat/coding-standard to v7.2.1 --- build-cs/composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 6e7fb818..4bcc8de4 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -195,16 +195,16 @@ }, { "name": "slevomat/coding-standard", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071" + "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b4f96a8beea515d2d89141b7b9ad72f526d84071", - "reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/aff06ae7a84e4534bf6f821dc982a93a5d477c90", + "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90", "shasum": "" }, "require": { @@ -216,7 +216,7 @@ "require-dev": { "phing/phing": "2.17.3", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.6.7", + "phpstan/phpstan": "1.4.10|1.7.1", "phpstan/phpstan-deprecation-rules": "1.0.0", "phpstan/phpstan-phpunit": "1.0.0|1.1.1", "phpstan/phpstan-strict-rules": "1.2.3", @@ -240,7 +240,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.2.0" + "source": "https://github.com/slevomat/coding-standard/tree/7.2.1" }, "funding": [ { @@ -252,7 +252,7 @@ "type": "tidelift" } ], - "time": "2022-05-06T10:58:42+00:00" + "time": "2022-05-25T10:58:12+00:00" }, { "name": "squizlabs/php_codesniffer", From 05098724e1476bb02a4eadf9c83b70584703a196 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 28 Jun 2022 23:05:02 +0200 Subject: [PATCH 145/277] Require PHPStan 1.8.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2f67ed5a..9e9279da 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.5.0" + "phpstan/phpstan": "^1.8.0" }, "conflict": { "phpunit/phpunit": "<7.0" From 34a6bb5c5427955ec02ab95a60cf3cf4cb4b92ec Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 28 Jun 2022 23:05:37 +0200 Subject: [PATCH 146/277] PHPStan baseline --- Makefile | 4 ++++ phpstan-baseline.neon | 11 +++++++++++ phpstan.neon | 1 + 3 files changed, 16 insertions(+) create mode 100644 phpstan-baseline.neon diff --git a/Makefile b/Makefile index fe917d3b..b34d0fec 100644 --- a/Makefile +++ b/Makefile @@ -21,3 +21,7 @@ cs-fix: .PHONY: phpstan phpstan: php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests + +.PHONY: phpstan-generate-baseline +phpstan-generate-baseline: + php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests -b phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..f0464def --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,11 @@ +parameters: + ignoreErrors: + - + message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php + + - + message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeStaticMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php diff --git a/phpstan.neon b/phpstan.neon index d71fc5ab..d1a581ac 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,7 @@ includes: - rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon - phar://phpstan.phar/conf/bleedingEdge.neon + - phpstan-baseline.neon parameters: excludePaths: From 2ca1b46190275fda8d9c9d8bb1b16fdd2b2c9c47 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 28 Jun 2022 23:11:54 +0200 Subject: [PATCH 147/277] Update .gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 615bf05f..9d7c518b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,4 +10,5 @@ tmp export-ignore Makefile export-ignore phpcs.xml export-ignore phpstan.neon export-ignore +phpstan-baseline.neon export-ignore phpunit.xml export-ignore From 52bdce81af70c8c3b1cb995e7442ca64aed562ef Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 13 Jul 2022 12:53:45 +0200 Subject: [PATCH 148/277] Create tag workflow --- .github/workflows/create-tag.yml | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/create-tag.yml diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml new file mode 100644 index 00000000..8452d986 --- /dev/null +++ b/.github/workflows/create-tag.yml @@ -0,0 +1,53 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Create tag" + +on: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch + workflow_dispatch: + inputs: + version: + description: 'Next version' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + +jobs: + create-tag: + name: "Create tag" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + + - name: 'Get Previous tag' + id: previoustag + uses: "WyriHaximus/github-action-get-previous-tag@v1" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: 'Get next versions' + id: semvers + uses: "WyriHaximus/github-action-next-semvers@v1" + with: + version: ${{ steps.previoustag.outputs.tag }} + + - name: "Create new minor tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'minor' + with: + tag: ${{ steps.semvers.outputs.minor }} + message: ${{ steps.semvers.outputs.minor }} + + - name: "Create new patch tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'patch' + with: + tag: ${{ steps.semvers.outputs.patch }} + message: ${{ steps.semvers.outputs.patch }} From b808cb8375c52ff611ce091aae666e1acf924d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 19 Jul 2022 13:16:01 +0200 Subject: [PATCH 149/277] Update build.yml --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 82332bb6..35254989 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,7 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" steps: - name: "Checkout" @@ -85,6 +86,7 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" dependencies: - "lowest" - "highest" @@ -127,6 +129,7 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" dependencies: - "lowest" - "highest" From d963a070beba9bac9f32f209111299bdf275b687 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 3 Oct 2022 15:23:24 +0200 Subject: [PATCH 150/277] Fix build --- composer.json | 2 +- phpstan.neon | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 9e9279da..a7ec4277 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.0" + "phpstan/phpstan": "^1.9.0" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index d1a581ac..2b8fa1ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,20 +8,3 @@ includes: parameters: excludePaths: - tests/*/data/* - -services: - scopeIsInClass: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInClass - removeNullMethodName: getClassReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - scopeIsInTrait: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInTrait - removeNullMethodName: getTraitReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension From 5fcfe8f8d79d97848064569c087193b65575b9e2 Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:10:17 +0100 Subject: [PATCH 151/277] Rules to check `@covers` and `@coversDefaultClass` for methods and classes --- extension.neon | 2 + rules.neon | 10 ++ src/Rules/PHPUnit/ClassCoversExistsRule.php | 93 ++++++++++++++ .../PHPUnit/ClassMethodCoversExistsRule.php | 118 ++++++++++++++++++ src/Rules/PHPUnit/CoversHelper.php | 101 +++++++++++++++ .../PHPUnit/ClassCoversExistsRuleTest.php | 48 +++++++ .../ClassMethodCoversExistsRuleTest.php | 65 ++++++++++ tests/Rules/PHPUnit/data/class-coverage.php | 25 ++++ tests/Rules/PHPUnit/data/method-coverage.php | 87 +++++++++++++ 9 files changed, 549 insertions(+) create mode 100644 src/Rules/PHPUnit/ClassCoversExistsRule.php create mode 100644 src/Rules/PHPUnit/ClassMethodCoversExistsRule.php create mode 100644 src/Rules/PHPUnit/CoversHelper.php create mode 100644 tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php create mode 100644 tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/class-coverage.php create mode 100644 tests/Rules/PHPUnit/data/method-coverage.php diff --git a/extension.neon b/extension.neon index 93d3a9ed..d3274e55 100644 --- a/extension.neon +++ b/extension.neon @@ -51,6 +51,8 @@ services: class: PHPStan\Type\PHPUnit\MockObjectDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Rules\PHPUnit\CoversHelper conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: diff --git a/rules.neon b/rules.neon index 5be69279..11aa3c04 100644 --- a/rules.neon +++ b/rules.neon @@ -4,3 +4,13 @@ rules: - PHPStan\Rules\PHPUnit\AssertSameWithCountRule - PHPStan\Rules\PHPUnit\MockMethodCallRule - PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule + +services: + - class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule + - class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule + +conditionalTags: + PHPStan\Rules\PHPUnit\ClassCoversExistsRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% diff --git a/src/Rules/PHPUnit/ClassCoversExistsRule.php b/src/Rules/PHPUnit/ClassCoversExistsRule.php new file mode 100644 index 00000000..a5a0cb70 --- /dev/null +++ b/src/Rules/PHPUnit/ClassCoversExistsRule.php @@ -0,0 +1,93 @@ + + */ +class ClassCoversExistsRule implements Rule +{ + + /** + * Covers helper. + * + * @var CoversHelper + */ + private $coversHelper; + + /** + * Reflection provider. + * + * @var ReflectionProvider + */ + private $reflectionProvider; + + public function __construct( + CoversHelper $coversHelper, + ReflectionProvider $reflectionProvider + ) + { + $this->reflectionProvider = $reflectionProvider; + $this->coversHelper = $coversHelper; + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + if (!$classReflection->isSubclassOf(TestCase::class)) { + return []; + } + + $errors = []; + $classPhpDoc = $classReflection->getResolvedPhpDoc(); + [$classCovers, $classCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($classPhpDoc); + + if (count($classCoversDefaultClasses) >= 2) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@coversDefaultClass is defined multiple times.' + ))->build(); + + return $errors; + } + + $coversDefaultClass = array_shift($classCoversDefaultClasses); + + if ($coversDefaultClass !== null) { + $className = (string) $coversDefaultClass->value; + if (!$this->reflectionProvider->hasClass($className)) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@coversDefaultClass references an invalid class %s.', + $className + ))->build(); + } + } + + foreach ($classCovers as $covers) { + $errors = array_merge( + $errors, + $this->coversHelper->processCovers($node, $covers, null) + ); + } + + return $errors; + } + +} diff --git a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php new file mode 100644 index 00000000..95e6cc89 --- /dev/null +++ b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php @@ -0,0 +1,118 @@ + + */ +class ClassMethodCoversExistsRule implements Rule +{ + + /** + * Covers helper. + * + * @var CoversHelper + */ + private $coversHelper; + + /** + * The file type mapper. + * + * @var FileTypeMapper + */ + private $fileTypeMapper; + + public function __construct( + CoversHelper $coversHelper, + FileTypeMapper $fileTypeMapper + ) + { + $this->coversHelper = $coversHelper; + $this->fileTypeMapper = $fileTypeMapper; + } + + public function getNodeType(): string + { + return Node\Stmt\ClassMethod::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + + if ($classReflection === null) { + return []; + } + + if (!$classReflection->isSubclassOf(TestCase::class)) { + return []; + } + + $errors = []; + $classPhpDoc = $classReflection->getResolvedPhpDoc(); + [$classCovers, $classCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($classPhpDoc); + + $classCoversStrings = array_map(static function (PhpDocTagNode $covers): string { + return (string) $covers->value; + }, $classCovers); + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $coversDefaultClass = count($classCoversDefaultClasses) === 1 + ? array_shift($classCoversDefaultClasses) + : null; + + $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $node->name->toString(), + $docComment->getText() + ); + + [$methodCovers, $methodCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($methodPhpDoc); + + $errors = []; + + if (count($methodCoversDefaultClasses) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@coversDefaultClass defined on class method %s.', + $node->name + ))->build(); + } + + foreach ($methodCovers as $covers) { + if (in_array((string) $covers->value, $classCoversStrings, true)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Class already @covers %s so the method @covers is redundant.', + $covers->value + ))->build(); + } + + $errors = array_merge( + $errors, + $this->coversHelper->processCovers($node, $covers, $coversDefaultClass) + ); + } + + return $errors; + } + +} diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php new file mode 100644 index 00000000..aebe000a --- /dev/null +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -0,0 +1,101 @@ +reflectionProvider = $reflectionProvider; + } + + /** + * Gathers @covers and @coversDefaultClass annotations from phpdocs. + * + * @return array{PhpDocTagNode[], PhpDocTagNode[]} + */ + public function getCoverAnnotations(?ResolvedPhpDocBlock $phpDoc): array + { + if ($phpDoc === null) { + return [[], []]; + } + + $phpDocNodes = $phpDoc->getPhpDocNodes(); + + $covers = []; + $coversDefaultClasses = []; + + foreach ($phpDocNodes as $docNode) { + $covers = array_merge( + $covers, + $docNode->getTagsByName('@covers') + ); + + $coversDefaultClasses = array_merge( + $coversDefaultClasses, + $docNode->getTagsByName('@coversDefaultClass') + ); + } + + return [$covers, $coversDefaultClasses]; + } + + /** + * @return RuleError[] errors + */ + public function processCovers( + Node $node, + PhpDocTagNode $phpDocTag, + ?PhpDocTagNode $coversDefaultClass + ): array + { + $errors = []; + $covers = (string) $phpDocTag->value; + + if (strpos($covers, '::') !== false) { + [$className, $method] = explode('::', $covers); + } else { + $className = $covers; + } + + if ($className === '' && $node instanceof Node\Stmt\ClassMethod && $coversDefaultClass !== null) { + $className = (string) $coversDefaultClass->value; + } + + if ($this->reflectionProvider->hasClass($className)) { + $class = $this->reflectionProvider->getClass($className); + if (isset($method) && $method !== '' && !$class->hasMethod($method)) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@covers value %s references an invalid method.', + $covers + ))->build(); + } + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + '@covers value %s references an invalid class.', + $covers + ))->build(); + } + return $errors; + } + +} diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php new file mode 100644 index 00000000..fb586555 --- /dev/null +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -0,0 +1,48 @@ + + */ +class ClassCoversExistsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflection = $this->createReflectionProvider(); + + return new ClassCoversExistsRule( + new CoversHelper($reflection), + $reflection + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/class-coverage.php'], [ + [ + '@coversDefaultClass references an invalid class \Not\A\Class.', + 8, + ], + [ + '@coversDefaultClass is defined multiple times.', + 23, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php new file mode 100644 index 00000000..2e093269 --- /dev/null +++ b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php @@ -0,0 +1,65 @@ + + */ +class ClassMethodCoversExistsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflection = $this->createReflectionProvider(); + + return new ClassMethodCoversExistsRule( + new CoversHelper($reflection), + self::getContainer()->getByType(FileTypeMapper::class) + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-coverage.php'], [ + [ + '@covers value ::ignoreThis references an invalid class.', + 14, + ], + [ + '@covers value \PHPUnit\Framework\TestCase::assertNotReal references an invalid method.', + 28, + ], + [ + '@covers value \Not\A\Class::foo references an invalid class.', + 35, + ], + [ + '@coversDefaultClass defined on class method testBadCoversDefault.', + 50, + ], + [ + '@covers value ::assertNotReal references an invalid method.', + 62, + ], + [ + 'Class already @covers \PHPUnit\Framework\TestCase so the method @covers is redundant.', + 85, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php new file mode 100644 index 00000000..c35cd26e --- /dev/null +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -0,0 +1,25 @@ + Date: Mon, 24 Oct 2022 13:23:32 +0200 Subject: [PATCH 152/277] Implement assertEmpty extension --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 11 +++++++++++ tests/Type/PHPUnit/data/assert-function.php | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 36203e65..4444db3f 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -3,6 +3,8 @@ namespace PHPStan\Type\PHPUnit\Assert; use Closure; +use Countable; +use EmptyIterator; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; @@ -156,6 +158,15 @@ private static function getExpressionResolvers(): array new ConstFetch(new Name('null')) ); }, + 'Empty' => static function (Scope $scope, Arg $actual): Expr\BinaryOp\BooleanOr { + return new Expr\BinaryOp\BooleanOr( + new Instanceof_($actual->value, new Name(EmptyIterator::class)), + new Expr\BinaryOp\BooleanOr( + new Instanceof_($actual->value, new Name(Countable::class)), + new Expr\Empty_($actual->value) + ) + ); + }, 'IsArray' => static function (Scope $scope, Arg $actual): FuncCall { return new FuncCall(new Name('is_array'), [$actual]); }, diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index ebfeb93a..117179ae 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -4,6 +4,7 @@ use function PHPStan\Testing\assertType; use function PHPUnit\Framework\assertArrayHasKey; +use function PHPUnit\Framework\assertEmpty; use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertObjectHasAttribute; @@ -36,4 +37,10 @@ public function objectHasAttribute(object $a): void assertType("object&hasProperty(property)", $a); } + public function testEmpty($a): void + { + assertEmpty($a); + assertType("0|0.0|''|'0'|array{}|Countable|EmptyIterator|false|null", $a); + } + } From 6b93db7fae6d6f3e81a5b4297f93af6fe4146785 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Oct 2022 13:38:17 +0200 Subject: [PATCH 153/277] Fix assertEmpty --- .../PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 4444db3f..e2684062 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -13,6 +13,7 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Name; +use PhpParser\Node\Scalar\LNumber; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -162,7 +163,10 @@ private static function getExpressionResolvers(): array return new Expr\BinaryOp\BooleanOr( new Instanceof_($actual->value, new Name(EmptyIterator::class)), new Expr\BinaryOp\BooleanOr( - new Instanceof_($actual->value, new Name(Countable::class)), + new Expr\BinaryOp\BooleanAnd( + new Instanceof_($actual->value, new Name(Countable::class)), + new Identical(new FuncCall(new Name('count'), [new Arg($actual->value)]), new LNumber(0)) + ), new Expr\Empty_($actual->value) ) ); From a0c136455f696d32d632c4994ea2d84df72792e7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 13:41:10 +0200 Subject: [PATCH 154/277] Regression tests --- .../ImpossibleCheckTypeMethodCallRuleTest.php | 45 +++++++++++++++++++ .../data/impossible-assert-method-call.php | 23 ++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/impossible-assert-method-call.php diff --git a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php new file mode 100644 index 00000000..b50bcfce --- /dev/null +++ b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php @@ -0,0 +1,45 @@ + + */ +class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(ImpossibleCheckTypeMethodCallRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/impossible-assert-method-call.php'], [ + [ + 'Call to method PHPUnit\Framework\Assert::assertEmpty() with array{} will always evaluate to true.', + 14, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertEmpty() with array{1, 2, 3} will always evaluate to false.', + 15, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + __DIR__ . '/../../../vendor/phpstan/phpstan-strict-rules/rules.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/data/impossible-assert-method-call.php b/tests/Rules/PHPUnit/data/impossible-assert-method-call.php new file mode 100644 index 00000000..a406f1e5 --- /dev/null +++ b/tests/Rules/PHPUnit/data/impossible-assert-method-call.php @@ -0,0 +1,23 @@ +assertEmpty($c); + $this->assertEmpty([]); + $this->assertEmpty([1, 2, 3]); + } + + public function doBar(object $o): void + { + $this->assertEmpty($o); + } + +} From f92aab7b75ebbcc5b59b012de1b12cadf5286b6f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 13:43:56 +0200 Subject: [PATCH 155/277] Regression test --- .../ImpossibleCheckTypeMethodCallRuleTest.php | 10 ++++ tests/Rules/PHPUnit/data/bug-141.php | 53 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests/Rules/PHPUnit/data/bug-141.php diff --git a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php index b50bcfce..700f420d 100644 --- a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php @@ -31,6 +31,16 @@ public function testRule(): void ]); } + public function testBug141(): void + { + $this->analyse([__DIR__ . '/data/bug-141.php'], [ + [ + "Call to method PHPUnit\Framework\Assert::assertEmpty() with non-empty-array<'0.6.0'|'1.0.0'|'1.0.x-dev'|'1.1.x-dev'|'9999999-dev'|'dev-feature-b', true> will always evaluate to false.", + 23, + ], + ]); + } + /** * @return string[] */ diff --git a/tests/Rules/PHPUnit/data/bug-141.php b/tests/Rules/PHPUnit/data/bug-141.php new file mode 100644 index 00000000..30011049 --- /dev/null +++ b/tests/Rules/PHPUnit/data/bug-141.php @@ -0,0 +1,53 @@ + $a + */ + public function doFoo(array $a): void + { + $this->assertEmpty($a); + } + + /** + * @param non-empty-array<'0.6.0'|'1.0.0'|'1.0.x-dev'|'1.1.x-dev'|'9999999-dev'|'dev-feature-b', true> $a + */ + public function doBar(array $a): void + { + $this->assertEmpty($a); + } + + public function doBaz(): void + { + $expected = [ + '0.6.0' => true, + '1.0.0' => true, + '1.0.x-dev' => true, + '1.1.x-dev' => true, + 'dev-feature-b' => true, + 'dev-feature/a-1.0-B' => true, + 'dev-master' => true, + '9999999-dev' => true, // alias of dev-master + ]; + + /** @var array */ + $packages = ['0.6.0', '1.0.0', '1']; + + foreach ($packages as $version) { + if (isset($expected[$version])) { + unset($expected[$version]); + } else { + throw new \Exception('Unexpected version '.$version); + } + } + + $this->assertEmpty($expected); + } + +} From 68017cc5780866ed85e14eba824c29c3032f00fa Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 13:46:44 +0200 Subject: [PATCH 156/277] Fix build --- phpstan-baseline.neon | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f0464def..53fb96b8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9,3 +9,8 @@ parameters: message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeStaticMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" count: 1 path: tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php + + - + message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php From 9b88cef57c5a5e8ddb90b69da26edd0d81eeadad Mon Sep 17 00:00:00 2001 From: PrinsFrank <25006490+PrinsFrank@users.noreply.github.com> Date: Wed, 26 Oct 2022 19:42:19 +0200 Subject: [PATCH 157/277] Add rule that checks for invalid and unrecognized annotations --- extension.neon | 2 + rules.neon | 6 ++ src/Rules/PHPUnit/AnnotationHelper.php | 66 ++++++++++++++ .../NoMissingSpaceInClassAnnotationRule.php | 49 +++++++++++ .../NoMissingSpaceInMethodAnnotationRule.php | 49 +++++++++++ ...oMissingSpaceInClassAnnotationRuleTest.php | 87 +++++++++++++++++++ ...MissingSpaceInMethodAnnotationRuleTest.php | 87 +++++++++++++++++++ .../data/InvalidClassCoversAnnotation.php | 38 ++++++++ .../data/InvalidMethodCoversAnnotation.php | 83 ++++++++++++++++++ 9 files changed, 467 insertions(+) create mode 100644 src/Rules/PHPUnit/AnnotationHelper.php create mode 100644 src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php create mode 100644 src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php create mode 100644 tests/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRuleTest.php create mode 100644 tests/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/InvalidClassCoversAnnotation.php create mode 100644 tests/Rules/PHPUnit/data/InvalidMethodCoversAnnotation.php diff --git a/extension.neon b/extension.neon index d3274e55..f6f372eb 100644 --- a/extension.neon +++ b/extension.neon @@ -53,6 +53,8 @@ services: - phpstan.broker.dynamicMethodReturnTypeExtension - class: PHPStan\Rules\PHPUnit\CoversHelper + - + class: PHPStan\Rules\PHPUnit\AnnotationHelper conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: diff --git a/rules.neon b/rules.neon index 11aa3c04..24a28ea0 100644 --- a/rules.neon +++ b/rules.neon @@ -8,9 +8,15 @@ rules: services: - class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule - class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule + - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule + - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule conditionalTags: PHPStan\Rules\PHPUnit\ClassCoversExistsRule: phpstan.rules.rule: %featureToggles.bleedingEdge% PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule: phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% diff --git a/src/Rules/PHPUnit/AnnotationHelper.php b/src/Rules/PHPUnit/AnnotationHelper.php new file mode 100644 index 00000000..f5529a8f --- /dev/null +++ b/src/Rules/PHPUnit/AnnotationHelper.php @@ -0,0 +1,66 @@ +getText()); + if ($docCommentLines === false) { + return []; + } + + foreach ($docCommentLines as $docCommentLine) { + // These annotations can't be retrieved using the getResolvedPhpDoc method on the FileTypeMapper as they are not present when they are invalid + $annotation = preg_match('/(?@(?[a-zA-Z]+)(?\s*)(?.*))/', $docCommentLine, $matches); + if ($annotation === false) { + continue; // Line without annotation + } + + if (array_key_exists('property', $matches) === false || array_key_exists('whitespace', $matches) === false || array_key_exists('annotation', $matches) === false) { + continue; + } + + if (!in_array($matches['property'], self::ANNOTATIONS_WITH_PARAMS, true) || $matches['whitespace'] !== '') { + continue; + } + + $errors[] = RuleErrorBuilder::message( + 'Annotation "' . $matches['annotation'] . '" is invalid, "@' . $matches['property'] . '" should be followed by a space and a value.' + )->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php b/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php new file mode 100644 index 00000000..89e3e8ff --- /dev/null +++ b/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php @@ -0,0 +1,49 @@ + + */ +class NoMissingSpaceInClassAnnotationRule implements Rule +{ + + /** + * Covers helper. + * + * @var AnnotationHelper + */ + private $annotationHelper; + + public function __construct(AnnotationHelper $annotationHelper) + { + $this->annotationHelper = $annotationHelper; + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + return $this->annotationHelper->processDocComment($docComment); + } + +} diff --git a/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php b/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php new file mode 100644 index 00000000..77577206 --- /dev/null +++ b/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php @@ -0,0 +1,49 @@ + + */ +class NoMissingSpaceInMethodAnnotationRule implements Rule +{ + + /** + * Covers helper. + * + * @var AnnotationHelper + */ + private $annotationHelper; + + public function __construct(AnnotationHelper $annotationHelper) + { + $this->annotationHelper = $annotationHelper; + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + return $this->annotationHelper->processDocComment($docComment); + } + +} diff --git a/tests/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRuleTest.php b/tests/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRuleTest.php new file mode 100644 index 00000000..e28fde15 --- /dev/null +++ b/tests/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRuleTest.php @@ -0,0 +1,87 @@ + + */ +class NoMissingSpaceInClassAnnotationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoMissingSpaceInClassAnnotationRule(new AnnotationHelper()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/InvalidClassCoversAnnotation.php'], [ + [ + 'Annotation "@backupGlobals" is invalid, "@backupGlobals" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@backupStaticAttributes" is invalid, "@backupStaticAttributes" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@covers\Dummy\Foo::assertSame" is invalid, "@covers" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@covers::assertSame" is invalid, "@covers" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@coversDefaultClass\Dummy\Foo" is invalid, "@coversDefaultClass" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@dataProvider" is invalid, "@dataProvider" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@depends" is invalid, "@depends" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@preserveGlobalState" is invalid, "@preserveGlobalState" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@requires" is invalid, "@requires" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@testDox" is invalid, "@testDox" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@testWith" is invalid, "@testWith" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@ticket" is invalid, "@ticket" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@uses" is invalid, "@uses" should be followed by a space and a value.', + 36, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRuleTest.php b/tests/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRuleTest.php new file mode 100644 index 00000000..2926ec93 --- /dev/null +++ b/tests/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRuleTest.php @@ -0,0 +1,87 @@ + + */ +class NoMissingSpaceInMethodAnnotationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoMissingSpaceInMethodAnnotationRule(new AnnotationHelper()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/InvalidMethodCoversAnnotation.php'], [ + [ + 'Annotation "@backupGlobals" is invalid, "@backupGlobals" should be followed by a space and a value.', + 12, + ], + [ + 'Annotation "@backupStaticAttributes" is invalid, "@backupStaticAttributes" should be followed by a space and a value.', + 19, + ], + [ + 'Annotation "@covers\Dummy\Foo::assertSame" is invalid, "@covers" should be followed by a space and a value.', + 27, + ], + [ + 'Annotation "@covers::assertSame" is invalid, "@covers" should be followed by a space and a value.', + 27, + ], + [ + 'Annotation "@coversDefaultClass\Dummy\Foo" is invalid, "@coversDefaultClass" should be followed by a space and a value.', + 33, + ], + [ + 'Annotation "@dataProvider" is invalid, "@dataProvider" should be followed by a space and a value.', + 39, + ], + [ + 'Annotation "@depends" is invalid, "@depends" should be followed by a space and a value.', + 45, + ], + [ + 'Annotation "@preserveGlobalState" is invalid, "@preserveGlobalState" should be followed by a space and a value.', + 52, + ], + [ + 'Annotation "@requires" is invalid, "@requires" should be followed by a space and a value.', + 58, + ], + [ + 'Annotation "@testDox" is invalid, "@testDox" should be followed by a space and a value.', + 64, + ], + [ + 'Annotation "@testWith" is invalid, "@testWith" should be followed by a space and a value.', + 70, + ], + [ + 'Annotation "@ticket" is invalid, "@ticket" should be followed by a space and a value.', + 76, + ], + [ + 'Annotation "@uses" is invalid, "@uses" should be followed by a space and a value.', + 82, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/data/InvalidClassCoversAnnotation.php b/tests/Rules/PHPUnit/data/InvalidClassCoversAnnotation.php new file mode 100644 index 00000000..11f9b904 --- /dev/null +++ b/tests/Rules/PHPUnit/data/InvalidClassCoversAnnotation.php @@ -0,0 +1,38 @@ += 5.3 + * @testDox + * @testDox foo bar + * @testWith + * @testWith ['foo', 'bar'] + * @ticket + * @ticket 1234 + * @uses + * @uses foo + */ +class InvalidClassCoversAnnotation extends \PHPUnit\Framework\TestCase +{ +} diff --git a/tests/Rules/PHPUnit/data/InvalidMethodCoversAnnotation.php b/tests/Rules/PHPUnit/data/InvalidMethodCoversAnnotation.php new file mode 100644 index 00000000..9154937b --- /dev/null +++ b/tests/Rules/PHPUnit/data/InvalidMethodCoversAnnotation.php @@ -0,0 +1,83 @@ += 5.3 + */ + public function requiresAnnotation() {} + + /** + * @testDox + * @testDox foo bar + */ + public function testDox() {} + + /** + * @testWith + * @testWith ['foo', 'bar'] + */ + public function testWith() {} + + /** + * @ticket + * @ticket 1234 + */ + public function ticket() {} + + /** + * @uses + * @uses foo + */ + public function uses() {} +} From 09b5c9ab38d4d601bcff04b4c9240832b86f5dda Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 20:02:49 +0200 Subject: [PATCH 158/277] Do not require PHPStan 1.9.0 yet This reverts commit d963a070beba9bac9f32f209111299bdf275b687. --- composer.json | 2 +- phpstan.neon | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a7ec4277..40683ead 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.0" + "phpstan/phpstan": "^1.8.11" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index 2b8fa1ae..d1a581ac 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,3 +8,20 @@ includes: parameters: excludePaths: - tests/*/data/* + +services: + scopeIsInClass: + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInClass + removeNullMethodName: getClassReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + + scopeIsInTrait: + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInTrait + removeNullMethodName: getTraitReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension From 273ca67d02379de5d563163a9ed74af2006ddf16 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 21:41:48 +0200 Subject: [PATCH 159/277] Revert "Do not require PHPStan 1.9.0 yet" This reverts commit 09b5c9ab38d4d601bcff04b4c9240832b86f5dda. --- composer.json | 2 +- phpstan.neon | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 40683ead..a7ec4277 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.11" + "phpstan/phpstan": "^1.9.0" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index d1a581ac..2b8fa1ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,20 +8,3 @@ includes: parameters: excludePaths: - tests/*/data/* - -services: - scopeIsInClass: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInClass - removeNullMethodName: getClassReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - scopeIsInTrait: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInTrait - removeNullMethodName: getTraitReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension From e431a6c9ed129f1f06281e33da957414a3dab196 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 20:02:49 +0200 Subject: [PATCH 160/277] Do not require PHPStan 1.9.0 yet This reverts commit d963a070beba9bac9f32f209111299bdf275b687. --- composer.json | 2 +- phpstan.neon | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a7ec4277..40683ead 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.0" + "phpstan/phpstan": "^1.8.11" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index 2b8fa1ae..d1a581ac 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,3 +8,20 @@ includes: parameters: excludePaths: - tests/*/data/* + +services: + scopeIsInClass: + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInClass + removeNullMethodName: getClassReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + + scopeIsInTrait: + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInTrait + removeNullMethodName: getTraitReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension From c86e460e93cfe0f2d39a175c71923257d5a07d7f Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:10:02 +0100 Subject: [PATCH 161/277] Fix covers rule for functions. --- src/Rules/PHPUnit/CoversHelper.php | 14 +++++++++++--- .../PHPUnit/ClassMethodCoversExistsRuleTest.php | 4 ++-- tests/Rules/PHPUnit/data/class-coverage.php | 12 ++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index aebe000a..23469469 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Name; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\Reflection\ReflectionProvider; @@ -70,8 +71,9 @@ public function processCovers( { $errors = []; $covers = (string) $phpDocTag->value; + $isMethod = strpos($covers, '::') !== false; - if (strpos($covers, '::') !== false) { + if ($isMethod) { [$className, $method] = explode('::', $covers); } else { $className = $covers; @@ -83,6 +85,7 @@ public function processCovers( if ($this->reflectionProvider->hasClass($className)) { $class = $this->reflectionProvider->getClass($className); + if (isset($method) && $method !== '' && !$class->hasMethod($method)) { $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid method.', @@ -90,9 +93,14 @@ public function processCovers( ))->build(); } } else { + if (!isset($method) && $this->reflectionProvider->hasFunction(new Name($covers, []), null)) { + return $errors; + } + $errors[] = RuleErrorBuilder::message(sprintf( - '@covers value %s references an invalid class.', - $covers + '@covers value %s references an invalid %s.', + $covers, + $isMethod ? 'method' : 'class or function' ))->build(); } return $errors; diff --git a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php index 2e093269..5764a605 100644 --- a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php @@ -26,7 +26,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/method-coverage.php'], [ [ - '@covers value ::ignoreThis references an invalid class.', + '@covers value ::ignoreThis references an invalid method.', 14, ], [ @@ -34,7 +34,7 @@ public function testRule(): void 28, ], [ - '@covers value \Not\A\Class::foo references an invalid class.', + '@covers value \Not\A\Class::foo references an invalid method.', 35, ], [ diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index c35cd26e..44ea6b45 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -23,3 +23,15 @@ class CoversShouldExistTestCase2 extends \PHPUnit\Framework\TestCase class MultipleCoversDefaultClass extends \PHPUnit\Framework\TestCase { } + +/** + * @covers \ClassCoverage\testable + */ +class CoversFunction extends \PHPUnit\Framework\TestCase +{ +} + +function testable(): void +{ + +} From dc086300d925208b2a62017f0264a9049a524adf Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:32:16 +0100 Subject: [PATCH 162/277] Add test case for class or function scenario. --- tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php | 4 ++++ tests/Rules/PHPUnit/data/class-coverage.php | 1 + 2 files changed, 5 insertions(+) diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php index fb586555..7ead3a7c 100644 --- a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -32,6 +32,10 @@ public function testRule(): void '@coversDefaultClass is defined multiple times.', 23, ], + [ + '@covers value \Not\A\Class references an invalid class or function.', + 31, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index 44ea6b45..fca878f2 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -26,6 +26,7 @@ class MultipleCoversDefaultClass extends \PHPUnit\Framework\TestCase /** * @covers \ClassCoverage\testable + * @covers \Not\A\Class */ class CoversFunction extends \PHPUnit\Framework\TestCase { From dea1f87344c6964c607d9076dee42d891f3923f0 Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:45:46 +0100 Subject: [PATCH 163/277] Be explicit with covers messaging when using @coversDefaultClass and @covers. --- src/Rules/PHPUnit/CoversHelper.php | 6 ++++-- tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index 23469469..8bcf92e4 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -72,6 +72,7 @@ public function processCovers( $errors = []; $covers = (string) $phpDocTag->value; $isMethod = strpos($covers, '::') !== false; + $fullName = $covers; if ($isMethod) { [$className, $method] = explode('::', $covers); @@ -81,6 +82,7 @@ public function processCovers( if ($className === '' && $node instanceof Node\Stmt\ClassMethod && $coversDefaultClass !== null) { $className = (string) $coversDefaultClass->value; + $fullName = $className . $covers; } if ($this->reflectionProvider->hasClass($className)) { @@ -89,7 +91,7 @@ public function processCovers( if (isset($method) && $method !== '' && !$class->hasMethod($method)) { $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid method.', - $covers + $fullName ))->build(); } } else { @@ -99,7 +101,7 @@ public function processCovers( $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid %s.', - $covers, + $fullName, $isMethod ? 'method' : 'class or function' ))->build(); } diff --git a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php index 5764a605..b886b460 100644 --- a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php @@ -26,7 +26,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/method-coverage.php'], [ [ - '@covers value ::ignoreThis references an invalid method.', + '@covers value \Not\A\Class::ignoreThis references an invalid method.', 14, ], [ @@ -42,7 +42,7 @@ public function testRule(): void 50, ], [ - '@covers value ::assertNotReal references an invalid method.', + '@covers value \PHPUnit\Framework\TestCase::assertNotReal references an invalid method.', 62, ], [ From 2de71f94c4a1114c4a6bf30d66a87439c0071338 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 28 Oct 2022 13:04:01 +0200 Subject: [PATCH 164/277] Revert "Do not require PHPStan 1.9.0 yet" This reverts commit e431a6c9ed129f1f06281e33da957414a3dab196. --- composer.json | 2 +- phpstan.neon | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 40683ead..a7ec4277 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.11" + "phpstan/phpstan": "^1.9.0" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index d1a581ac..2b8fa1ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,20 +8,3 @@ includes: parameters: excludePaths: - tests/*/data/* - -services: - scopeIsInClass: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInClass - removeNullMethodName: getClassReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - scopeIsInTrait: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInTrait - removeNullMethodName: getTraitReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension From a6aebda5b9206c61495801722b8329708a88a8c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Nov 2022 02:51:01 +0000 Subject: [PATCH 165/277] Update metcalfc/changelog-generator action to v4 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5fed0458..bac4a006 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v3.0.0 + uses: metcalfc/changelog-generator@v4.0.1 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 8313d41c08795f81925c9b951232f483c6efe21f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 01:04:45 +0000 Subject: [PATCH 166/277] Update dessant/lock-threads action to v4 --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index a05d4173..4c7990df 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -8,7 +8,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v3 + - uses: dessant/lock-threads@v4 with: github-token: ${{ github.token }} issue-inactive-days: '31' From 4c06b7e3f2c40081334d86975350dda814bd064a Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Wed, 7 Dec 2022 16:46:24 +0100 Subject: [PATCH 167/277] Add rule to check `@dataProvider` --- extension.neon | 2 + rules.neon | 6 ++ .../PHPUnit/DataProviderDeclarationRule.php | 90 ++++++++++++++++ src/Rules/PHPUnit/DataProviderHelper.php | 102 ++++++++++++++++++ .../DataProviderDeclarationRuleTest.php | 51 +++++++++ .../data/data-provider-declaration.php | 70 ++++++++++++ 6 files changed, 321 insertions(+) create mode 100644 src/Rules/PHPUnit/DataProviderDeclarationRule.php create mode 100644 src/Rules/PHPUnit/DataProviderHelper.php create mode 100644 tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/data-provider-declaration.php diff --git a/extension.neon b/extension.neon index f6f372eb..5c6a90d8 100644 --- a/extension.neon +++ b/extension.neon @@ -55,6 +55,8 @@ services: class: PHPStan\Rules\PHPUnit\CoversHelper - class: PHPStan\Rules\PHPUnit\AnnotationHelper + - + class: PHPStan\Rules\PHPUnit\DataProviderHelper conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: diff --git a/rules.neon b/rules.neon index 24a28ea0..195ace09 100644 --- a/rules.neon +++ b/rules.neon @@ -8,6 +8,10 @@ rules: services: - class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule - class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule + - + class: PHPStan\Rules\PHPUnit\DataProviderDeclarationRule + arguments: + checkFunctionNameCase: %checkFunctionNameCase% - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule @@ -16,6 +20,8 @@ conditionalTags: phpstan.rules.rule: %featureToggles.bleedingEdge% PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule: phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\PHPUnit\DataProviderDeclarationRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule: phpstan.rules.rule: %featureToggles.bleedingEdge% PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule: diff --git a/src/Rules/PHPUnit/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php new file mode 100644 index 00000000..7d1afd6e --- /dev/null +++ b/src/Rules/PHPUnit/DataProviderDeclarationRule.php @@ -0,0 +1,90 @@ + + */ +class DataProviderDeclarationRule implements Rule +{ + + /** + * Data provider helper. + * + * @var DataProviderHelper + */ + private $dataProviderHelper; + + /** + * The file type mapper. + * + * @var FileTypeMapper + */ + private $fileTypeMapper; + + /** + * When set to true, it reports data provider method with incorrect name case. + * + * @var bool + */ + private $checkFunctionNameCase; + + public function __construct( + DataProviderHelper $dataProviderHelper, + FileTypeMapper $fileTypeMapper, + bool $checkFunctionNameCase + ) + { + $this->dataProviderHelper = $dataProviderHelper; + $this->fileTypeMapper = $fileTypeMapper; + $this->checkFunctionNameCase = $checkFunctionNameCase; + } + + public function getNodeType(): string + { + return Node\Stmt\ClassMethod::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + + if ($classReflection === null || !$classReflection->isSubclassOf(TestCase::class)) { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $node->name->toString(), + $docComment->getText() + ); + + $annotations = $this->dataProviderHelper->getDataProviderAnnotations($methodPhpDoc); + + $errors = []; + + foreach ($annotations as $annotation) { + $errors = array_merge( + $errors, + $this->dataProviderHelper->processDataProvider($scope, $annotation, $this->checkFunctionNameCase) + ); + } + + return $errors; + } + +} diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php new file mode 100644 index 00000000..ef3bfcdc --- /dev/null +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -0,0 +1,102 @@ + + */ + public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array + { + if ($phpDoc === null) { + return []; + } + + $phpDocNodes = $phpDoc->getPhpDocNodes(); + + $annotations = []; + + foreach ($phpDocNodes as $docNode) { + $annotations = array_merge( + $annotations, + $docNode->getTagsByName('@dataProvider') + ); + } + + return $annotations; + } + + /** + * @return RuleError[] errors + */ + public function processDataProvider( + Scope $scope, + PhpDocTagNode $phpDocTag, + bool $checkFunctionNameCase + ): array + { + $dataProviderName = $this->getDataProviderName($phpDocTag); + if ($dataProviderName === null) { + // Missing name is already handled in NoMissingSpaceInMethodAnnotationRule + return []; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + // Should not happen + return []; + } + + try { + $dataProviderMethodReflection = $classReflection->getNativeMethod($dataProviderName); + } catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) { + $error = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related method not found.', + $dataProviderName + ))->build(); + + return [$error]; + } + + $errors = []; + + if ($checkFunctionNameCase && $dataProviderName !== $dataProviderMethodReflection->getName()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related method is used with incorrect case: %s.', + $dataProviderName, + $dataProviderMethodReflection->getName() + ))->build(); + } + + if (!$dataProviderMethodReflection->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related method must be public.', + $dataProviderName + ))->build(); + } + + return $errors; + } + + private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string + { + if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) { + return null; + } + + return $matches[0]; + } + +} diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php new file mode 100644 index 00000000..44434e3b --- /dev/null +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -0,0 +1,51 @@ + + */ +class DataProviderDeclarationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DataProviderDeclarationRule( + new DataProviderHelper(), + self::getContainer()->getByType(FileTypeMapper::class), + true + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [ + [ + '@dataProvider providebaz related method is used with incorrect case: provideBaz.', + 13, + ], + [ + '@dataProvider provideQuux related method must be public.', + 13, + ], + [ + '@dataProvider provideNonExisting related method not found.', + 66, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } +} diff --git a/tests/Rules/PHPUnit/data/data-provider-declaration.php b/tests/Rules/PHPUnit/data/data-provider-declaration.php new file mode 100644 index 00000000..2690d02b --- /dev/null +++ b/tests/Rules/PHPUnit/data/data-provider-declaration.php @@ -0,0 +1,70 @@ + Date: Wed, 7 Dec 2022 18:07:04 +0100 Subject: [PATCH 168/277] Report data providers deprecated usage --- composer.json | 2 +- rules.neon | 1 + .../PHPUnit/DataProviderDeclarationRule.php | 18 ++++++++++++++++-- src/Rules/PHPUnit/DataProviderHelper.php | 10 +++++++++- .../DataProviderDeclarationRuleTest.php | 5 +++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index a7ec4277..f3ce0e7f 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.0" + "phpstan/phpstan": "^1.9.3" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/rules.neon b/rules.neon index 195ace09..8dc7056b 100644 --- a/rules.neon +++ b/rules.neon @@ -12,6 +12,7 @@ services: class: PHPStan\Rules\PHPUnit\DataProviderDeclarationRule arguments: checkFunctionNameCase: %checkFunctionNameCase% + deprecationRulesInstalled: %deprecationRulesInstalled% - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule diff --git a/src/Rules/PHPUnit/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php index 7d1afd6e..612cf06d 100644 --- a/src/Rules/PHPUnit/DataProviderDeclarationRule.php +++ b/src/Rules/PHPUnit/DataProviderDeclarationRule.php @@ -36,15 +36,24 @@ class DataProviderDeclarationRule implements Rule */ private $checkFunctionNameCase; + /** + * When phpstan-deprecation-rules is installed, it reports deprecated usages. + * + * @var bool + */ + private $deprecationRulesInstalled; + public function __construct( DataProviderHelper $dataProviderHelper, FileTypeMapper $fileTypeMapper, - bool $checkFunctionNameCase + bool $checkFunctionNameCase, + bool $deprecationRulesInstalled ) { $this->dataProviderHelper = $dataProviderHelper; $this->fileTypeMapper = $fileTypeMapper; $this->checkFunctionNameCase = $checkFunctionNameCase; + $this->deprecationRulesInstalled = $deprecationRulesInstalled; } public function getNodeType(): string @@ -80,7 +89,12 @@ public function processNode(Node $node, Scope $scope): array foreach ($annotations as $annotation) { $errors = array_merge( $errors, - $this->dataProviderHelper->processDataProvider($scope, $annotation, $this->checkFunctionNameCase) + $this->dataProviderHelper->processDataProvider( + $scope, + $annotation, + $this->checkFunctionNameCase, + $this->deprecationRulesInstalled + ) ); } diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index ef3bfcdc..e88f2937 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -44,7 +44,8 @@ public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array public function processDataProvider( Scope $scope, PhpDocTagNode $phpDocTag, - bool $checkFunctionNameCase + bool $checkFunctionNameCase, + bool $deprecationRulesInstalled ): array { $dataProviderName = $this->getDataProviderName($phpDocTag); @@ -87,6 +88,13 @@ public function processDataProvider( ))->build(); } + if ($deprecationRulesInstalled && !$dataProviderMethodReflection->isStatic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related method must be static.', + $dataProviderName + ))->build(); + } + return $errors; } diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index 44434e3b..cfd27ae0 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -17,6 +17,7 @@ protected function getRule(): Rule return new DataProviderDeclarationRule( new DataProviderHelper(), self::getContainer()->getByType(FileTypeMapper::class), + true, true ); } @@ -28,6 +29,10 @@ public function testRule(): void '@dataProvider providebaz related method is used with incorrect case: provideBaz.', 13, ], + [ + '@dataProvider provideQux related method must be static.', + 13, + ], [ '@dataProvider provideQuux related method must be public.', 13, From bc0a2909b00eea7104999a4af88e339c2fafbb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Wer=C5=82os?= Date: Sun, 11 Dec 2022 12:33:47 +0100 Subject: [PATCH 169/277] Do not use "strtolower" when there is a dedicated method --- src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 3 +-- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 3 +-- src/Rules/PHPUnit/AssertSameWithCountRule.php | 7 +++---- src/Rules/PHPUnit/ShouldCallParentMethodsRule.php | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index d24d4904..284f53b9 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -10,7 +10,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use function count; -use function strtolower; /** * @implements Rule @@ -35,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array if (count($node->getArgs()) < 2) { return []; } - if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { + if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { return []; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 672f3496..ca8ac607 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -10,7 +10,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use function count; -use function strtolower; /** * @implements Rule @@ -35,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array if (count($node->getArgs()) < 2) { return []; } - if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { + if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { return []; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 1f1a3ab2..a0a79a96 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -11,7 +11,6 @@ use PHPStan\Rules\Rule; use PHPStan\Type\ObjectType; use function count; -use function strtolower; /** * @implements Rule @@ -36,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array if (count($node->getArgs()) < 2) { return []; } - if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { + if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { return []; } @@ -45,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array if ( $right instanceof Node\Expr\FuncCall && $right->name instanceof Node\Name - && strtolower($right->name->toString()) === 'count' + && $right->name->toLowerString() === 'count' ) { return [ 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', @@ -55,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array if ( $right instanceof Node\Expr\MethodCall && $right->name instanceof Node\Identifier - && strtolower($right->name->toString()) === 'count' + && $right->name->toLowerString() === 'count' && count($right->getArgs()) === 0 ) { $type = $scope->getType($right->var); diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index 01414061..5a640a1c 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -97,7 +97,7 @@ private function hasParentClassCall(?array $stmts, string $methodName): bool continue; } - if (strtolower($stmt->expr->name->name) === $methodName) { + if ($stmt->expr->name->toLowerString() === $methodName) { return true; } } From 008f5da032f441aa01b563972256808f9d8501c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Wer=C5=82os?= Date: Sun, 11 Dec 2022 16:46:20 +0100 Subject: [PATCH 170/277] Update .gitattributes --- .gitattributes | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.gitattributes b/.gitattributes index 9d7c518b..00d6b17d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,13 +2,12 @@ *.stub linguist-language=PHP *.neon linguist-language=YAML -.github export-ignore -tests export-ignore -tmp export-ignore -.gitattributes export-ignore -.gitignore export-ignore -Makefile export-ignore -phpcs.xml export-ignore -phpstan.neon export-ignore -phpstan-baseline.neon export-ignore -phpunit.xml export-ignore +/.* export-ignore +/build-cs export-ignore +/tests export-ignore +/tmp export-ignore +/Makefile export-ignore +/phpcs.xml export-ignore +/phpstan.neon export-ignore +/phpstan-baseline.neon export-ignore +/phpunit.xml export-ignore From b9827cf8df2bd97c7c07b1bb27c694ee41052754 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Mon, 12 Dec 2022 22:02:25 +0100 Subject: [PATCH 171/277] Discover data providers from other classes --- src/Rules/PHPUnit/DataProviderHelper.php | 63 +++++++++++++++---- .../DataProviderDeclarationRuleTest.php | 16 +++-- .../data/data-provider-declaration.php | 9 +++ 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index e88f2937..a224cb78 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -5,16 +5,32 @@ use PHPStan\Analyser\Scope; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MissingMethodFromReflectionException; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use function array_merge; +use function count; +use function explode; use function preg_match; use function sprintf; class DataProviderHelper { + /** + * Reflection provider. + * + * @var ReflectionProvider + */ + private $reflectionProvider; + + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + /** * @return array */ @@ -48,24 +64,28 @@ public function processDataProvider( bool $deprecationRulesInstalled ): array { - $dataProviderName = $this->getDataProviderName($phpDocTag); - if ($dataProviderName === null) { - // Missing name is already handled in NoMissingSpaceInMethodAnnotationRule + $dataProviderValue = $this->getDataProviderValue($phpDocTag); + if ($dataProviderValue === null) { + // Missing value is already handled in NoMissingSpaceInMethodAnnotationRule return []; } - $classReflection = $scope->getClassReflection(); + [$classReflection, $method] = $this->parseDataProviderValue($scope, $dataProviderValue); if ($classReflection === null) { - // Should not happen - return []; + $error = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related class not found.', + $dataProviderValue + ))->build(); + + return [$error]; } try { - $dataProviderMethodReflection = $classReflection->getNativeMethod($dataProviderName); + $dataProviderMethodReflection = $classReflection->getNativeMethod($method); } catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) { $error = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method not found.', - $dataProviderName + $dataProviderValue ))->build(); return [$error]; @@ -73,10 +93,10 @@ public function processDataProvider( $errors = []; - if ($checkFunctionNameCase && $dataProviderName !== $dataProviderMethodReflection->getName()) { + if ($checkFunctionNameCase && $method !== $dataProviderMethodReflection->getName()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method is used with incorrect case: %s.', - $dataProviderName, + $dataProviderValue, $dataProviderMethodReflection->getName() ))->build(); } @@ -84,21 +104,21 @@ public function processDataProvider( if (!$dataProviderMethodReflection->isPublic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be public.', - $dataProviderName + $dataProviderValue ))->build(); } if ($deprecationRulesInstalled && !$dataProviderMethodReflection->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be static.', - $dataProviderName + $dataProviderValue ))->build(); } return $errors; } - private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string + private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string { if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) { return null; @@ -107,4 +127,21 @@ private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string return $matches[0]; } + /** + * @return array{ClassReflection|null, string} + */ + private function parseDataProviderValue(Scope $scope, string $dataProviderValue): array + { + $parts = explode('::', $dataProviderValue, 2); + if (count($parts) <= 1) { + return [$scope->getClassReflection(), $dataProviderValue]; + } + + if ($this->reflectionProvider->hasClass($parts[0])) { + return [$this->reflectionProvider->getClass($parts[0]), $parts[1]]; + } + + return [null, $dataProviderValue]; + } + } diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index cfd27ae0..04dad8dc 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -14,8 +14,10 @@ class DataProviderDeclarationRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflection = $this->createReflectionProvider(); + return new DataProviderDeclarationRule( - new DataProviderHelper(), + new DataProviderHelper($reflection), self::getContainer()->getByType(FileTypeMapper::class), true, true @@ -27,19 +29,23 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [ [ '@dataProvider providebaz related method is used with incorrect case: provideBaz.', - 13, + 14, ], [ '@dataProvider provideQux related method must be static.', - 13, + 14, ], [ '@dataProvider provideQuux related method must be public.', - 13, + 14, ], [ '@dataProvider provideNonExisting related method not found.', - 66, + 68, + ], + [ + '@dataProvider NonExisting::provideNonExisting related class not found.', + 68, ], ]); } diff --git a/tests/Rules/PHPUnit/data/data-provider-declaration.php b/tests/Rules/PHPUnit/data/data-provider-declaration.php index 2690d02b..23af826b 100644 --- a/tests/Rules/PHPUnit/data/data-provider-declaration.php +++ b/tests/Rules/PHPUnit/data/data-provider-declaration.php @@ -9,6 +9,7 @@ class FooTestCase extends \PHPUnit\Framework\TestCase * @dataProvider providebaz * @dataProvider provideQux * @dataProvider provideQuux + * @dataProvider \ExampleTestCase\BarTestCase::provideToOtherClass */ public function testIsNotFoo(string $subject): void { @@ -61,10 +62,18 @@ class BarTestCase extends \PHPUnit\Framework\TestCase /** * @dataProvider provideNonExisting + * @dataProvider NonExisting::provideNonExisting * @dataProvider provideCorge */ public function testIsNotBar(string $subject): void { self::assertNotSame('bar', $subject); } + + public static function provideToOtherClass(): iterable + { + return [ + ['toOtherClass'], + ]; + } } From cd9c6938f8bbfcb6da3ed5a3c7ea60873825d088 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 13 Dec 2022 15:49:24 +0100 Subject: [PATCH 172/277] DataProviderDeclarationRule - report non-static dataProvider only with PHPUnit 10+ --- extension.neon | 3 ++ src/Rules/PHPUnit/DataProviderHelper.php | 10 ++-- .../PHPUnit/DataProviderHelperFactory.php | 52 +++++++++++++++++++ .../DataProviderDeclarationRuleTest.php | 4 +- 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/Rules/PHPUnit/DataProviderHelperFactory.php diff --git a/extension.neon b/extension.neon index 5c6a90d8..cea2b155 100644 --- a/extension.neon +++ b/extension.neon @@ -57,6 +57,9 @@ services: class: PHPStan\Rules\PHPUnit\AnnotationHelper - class: PHPStan\Rules\PHPUnit\DataProviderHelper + factory: @PHPStan\Rules\PHPUnit\DataProviderHelperFactory::create() + - + class: PHPStan\Rules\PHPUnit\DataProviderHelperFactory conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index a224cb78..30079600 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -26,9 +26,13 @@ class DataProviderHelper */ private $reflectionProvider; - public function __construct(ReflectionProvider $reflectionProvider) + /** @var bool */ + private $phpunit10OrNewer; + + public function __construct(ReflectionProvider $reflectionProvider, bool $phpunit10OrNewer) { $this->reflectionProvider = $reflectionProvider; + $this->phpunit10OrNewer = $phpunit10OrNewer; } /** @@ -108,9 +112,9 @@ public function processDataProvider( ))->build(); } - if ($deprecationRulesInstalled && !$dataProviderMethodReflection->isStatic()) { + if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( - '@dataProvider %s related method must be static.', + '@dataProvider %s related method must be static in PHPUnit 10 and newer.', $dataProviderValue ))->build(); } diff --git a/src/Rules/PHPUnit/DataProviderHelperFactory.php b/src/Rules/PHPUnit/DataProviderHelperFactory.php new file mode 100644 index 00000000..a93ecfd3 --- /dev/null +++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php @@ -0,0 +1,52 @@ +reflectionProvider = $reflectionProvider; + } + + public function create(): DataProviderHelper + { + $phpUnit10OrNewer = false; + if ($this->reflectionProvider->hasClass(TestCase::class)) { + $testCase = $this->reflectionProvider->getClass(TestCase::class); + $file = $testCase->getFileName(); + if ($file !== null) { + $phpUnitRoot = dirname($file, 3); + $phpUnitComposer = $phpUnitRoot . '/composer.json'; + if (is_file($phpUnitComposer)) { + $composerJson = @file_get_contents($phpUnitComposer); + if ($composerJson !== false) { + $json = json_decode($composerJson, true); + $version = $json['extra']['branch-alias']['dev-main'] ?? null; + if ($version !== null) { + $majorVersion = (int) explode('.', $version)[0]; + if ($majorVersion >= 10) { + $phpUnit10OrNewer = true; + } + } + } + } + } + } + + return new DataProviderHelper($this->reflectionProvider, $phpUnit10OrNewer); + } + +} diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index 04dad8dc..03c44ecb 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule $reflection = $this->createReflectionProvider(); return new DataProviderDeclarationRule( - new DataProviderHelper($reflection), + new DataProviderHelper($reflection, true), self::getContainer()->getByType(FileTypeMapper::class), true, true @@ -32,7 +32,7 @@ public function testRule(): void 14, ], [ - '@dataProvider provideQux related method must be static.', + '@dataProvider provideQux related method must be static in PHPUnit 10 and newer.', 14, ], [ From 64f4c56a19b0409c2c721378059d3a8eaa0f33a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Mon, 19 Dec 2022 13:25:26 +0100 Subject: [PATCH 173/277] Create release-toot.yml --- .github/workflows/release-toot.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/release-toot.yml diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml new file mode 100644 index 00000000..2af0f176 --- /dev/null +++ b/.github/workflows/release-toot.yml @@ -0,0 +1,21 @@ +name: Toot release + +# More triggers +# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release +on: + release: + types: [published] + +jobs: + toot: + runs-on: ubuntu-latest + steps: + - uses: cbrgm/mastodon-github-action@v1 + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" + env: + MASTODON_URL: phpc.social + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} From 7f7b59b560eac6d4eff06a2aaffc8b67dd166776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 20 Dec 2022 22:07:58 +0100 Subject: [PATCH 174/277] Update release-toot.yml --- .github/workflows/release-toot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml index 2af0f176..6a1c8156 100644 --- a/.github/workflows/release-toot.yml +++ b/.github/workflows/release-toot.yml @@ -17,5 +17,5 @@ jobs: # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" env: - MASTODON_URL: phpc.social + MASTODON_URL: https://phpc.social MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} From 54a24bd23e9e80ee918cdc24f909d376c2e273f7 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Wed, 21 Dec 2022 16:16:26 +0100 Subject: [PATCH 175/277] Add support for data provider attributes --- .../PHPUnit/DataProviderDeclarationRule.php | 33 +--- src/Rules/PHPUnit/DataProviderHelper.php | 164 +++++++++++++++--- .../PHPUnit/DataProviderHelperFactory.php | 9 +- .../DataProviderDeclarationRuleTest.php | 29 +++- .../data/data-provider-declaration.php | 14 ++ 5 files changed, 192 insertions(+), 57 deletions(-) diff --git a/src/Rules/PHPUnit/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php index 612cf06d..37c586de 100644 --- a/src/Rules/PHPUnit/DataProviderDeclarationRule.php +++ b/src/Rules/PHPUnit/DataProviderDeclarationRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPUnit\Framework\TestCase; use function array_merge; @@ -22,13 +21,6 @@ class DataProviderDeclarationRule implements Rule */ private $dataProviderHelper; - /** - * The file type mapper. - * - * @var FileTypeMapper - */ - private $fileTypeMapper; - /** * When set to true, it reports data provider method with incorrect name case. * @@ -45,13 +37,11 @@ class DataProviderDeclarationRule implements Rule public function __construct( DataProviderHelper $dataProviderHelper, - FileTypeMapper $fileTypeMapper, bool $checkFunctionNameCase, bool $deprecationRulesInstalled ) { $this->dataProviderHelper = $dataProviderHelper; - $this->fileTypeMapper = $fileTypeMapper; $this->checkFunctionNameCase = $checkFunctionNameCase; $this->deprecationRulesInstalled = $deprecationRulesInstalled; } @@ -69,29 +59,16 @@ public function processNode(Node $node, Scope $scope): array return []; } - $docComment = $node->getDocComment(); - if ($docComment === null) { - return []; - } - - $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $classReflection->getName(), - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $node->name->toString(), - $docComment->getText() - ); - - $annotations = $this->dataProviderHelper->getDataProviderAnnotations($methodPhpDoc); - $errors = []; - foreach ($annotations as $annotation) { + foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $node, $classReflection) as $dataProviderValue => [$dataProviderClassReflection, $dataProviderMethodName, $lineNumber]) { $errors = array_merge( $errors, $this->dataProviderHelper->processDataProvider( - $scope, - $annotation, + $dataProviderValue, + $dataProviderClassReflection, + $dataProviderMethodName, + $lineNumber, $this->checkFunctionNameCase, $this->deprecationRulesInstalled ) diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index 30079600..6651b051 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -2,6 +2,11 @@ namespace PHPStan\Rules\PHPUnit; +use PhpParser\Node\Attribute; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\ClassMethod; use PHPStan\Analyser\Scope; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; @@ -10,6 +15,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\FileTypeMapper; use function array_merge; use function count; use function explode; @@ -26,19 +32,84 @@ class DataProviderHelper */ private $reflectionProvider; + /** + * The file type mapper. + * + * @var FileTypeMapper + */ + private $fileTypeMapper; + /** @var bool */ private $phpunit10OrNewer; - public function __construct(ReflectionProvider $reflectionProvider, bool $phpunit10OrNewer) + public function __construct( + ReflectionProvider $reflectionProvider, + FileTypeMapper $fileTypeMapper, + bool $phpunit10OrNewer + ) { $this->reflectionProvider = $reflectionProvider; + $this->fileTypeMapper = $fileTypeMapper; $this->phpunit10OrNewer = $phpunit10OrNewer; } + /** + * @return iterable + */ + public function getDataProviderMethods( + Scope $scope, + ClassMethod $node, + ClassReflection $classReflection + ): iterable + { + $docComment = $node->getDocComment(); + if ($docComment !== null) { + $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $node->name->toString(), + $docComment->getText() + ); + foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) { + $dataProviderValue = $this->getDataProviderAnnotationValue($annotation); + if ($dataProviderValue === null) { + // Missing value is already handled in NoMissingSpaceInMethodAnnotationRule + continue; + } + + $dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue); + $dataProviderMethod[] = $node->getLine(); + + yield $dataProviderValue => $dataProviderMethod; + } + } + + if (!$this->phpunit10OrNewer) { + return; + } + + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $dataProviderMethod = null; + if ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataprovider') { + $dataProviderMethod = $this->parseDataProviderAttribute($attr, $classReflection); + } elseif ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataproviderexternal') { + $dataProviderMethod = $this->parseDataProviderExternalAttribute($attr); + } + if ($dataProviderMethod === null) { + continue; + } + + yield from $dataProviderMethod; + } + } + } + /** * @return array */ - public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array + private function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array { if ($phpDoc === null) { return []; @@ -62,67 +133,62 @@ public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array * @return RuleError[] errors */ public function processDataProvider( - Scope $scope, - PhpDocTagNode $phpDocTag, + string $dataProviderValue, + ?ClassReflection $classReflection, + string $methodName, + int $lineNumber, bool $checkFunctionNameCase, bool $deprecationRulesInstalled ): array { - $dataProviderValue = $this->getDataProviderValue($phpDocTag); - if ($dataProviderValue === null) { - // Missing value is already handled in NoMissingSpaceInMethodAnnotationRule - return []; - } - - [$classReflection, $method] = $this->parseDataProviderValue($scope, $dataProviderValue); if ($classReflection === null) { $error = RuleErrorBuilder::message(sprintf( '@dataProvider %s related class not found.', $dataProviderValue - ))->build(); + ))->line($lineNumber)->build(); return [$error]; } try { - $dataProviderMethodReflection = $classReflection->getNativeMethod($method); + $dataProviderMethodReflection = $classReflection->getNativeMethod($methodName); } catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) { $error = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method not found.', $dataProviderValue - ))->build(); + ))->line($lineNumber)->build(); return [$error]; } $errors = []; - if ($checkFunctionNameCase && $method !== $dataProviderMethodReflection->getName()) { + if ($checkFunctionNameCase && $methodName !== $dataProviderMethodReflection->getName()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method is used with incorrect case: %s.', $dataProviderValue, $dataProviderMethodReflection->getName() - ))->build(); + ))->line($lineNumber)->build(); } if (!$dataProviderMethodReflection->isPublic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be public.', $dataProviderValue - ))->build(); + ))->line($lineNumber)->build(); } if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be static in PHPUnit 10 and newer.', $dataProviderValue - ))->build(); + ))->line($lineNumber)->build(); } return $errors; } - private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string + private function getDataProviderAnnotationValue(PhpDocTagNode $phpDocTag): ?string { if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) { return null; @@ -134,7 +200,7 @@ private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string /** * @return array{ClassReflection|null, string} */ - private function parseDataProviderValue(Scope $scope, string $dataProviderValue): array + private function parseDataProviderAnnotationValue(Scope $scope, string $dataProviderValue): array { $parts = explode('::', $dataProviderValue, 2); if (count($parts) <= 1) { @@ -148,4 +214,62 @@ private function parseDataProviderValue(Scope $scope, string $dataProviderValue) return [null, $dataProviderValue]; } + /** + * @return array|null + */ + private function parseDataProviderExternalAttribute(Attribute $attribute): ?array + { + if (count($attribute->args) !== 2) { + return null; + } + $methodNameArg = $attribute->args[1]->value; + if (!$methodNameArg instanceof String_) { + return null; + } + $classNameArg = $attribute->args[0]->value; + if ($classNameArg instanceof ClassConstFetch && $classNameArg->class instanceof Name) { + $className = $classNameArg->class->toString(); + } elseif ($classNameArg instanceof String_) { + $className = $classNameArg->value; + } else { + return null; + } + + $dataProviderClassReflection = null; + if ($this->reflectionProvider->hasClass($className)) { + $dataProviderClassReflection = $this->reflectionProvider->getClass($className); + $className = $dataProviderClassReflection->getName(); + } + + return [ + sprintf('%s::%s', $className, $methodNameArg->value) => [ + $dataProviderClassReflection, + $methodNameArg->value, + $attribute->getLine(), + ], + ]; + } + + /** + * @return array|null + */ + private function parseDataProviderAttribute(Attribute $attribute, ClassReflection $classReflection): ?array + { + if (count($attribute->args) !== 1) { + return null; + } + $methodNameArg = $attribute->args[0]->value; + if (!$methodNameArg instanceof String_) { + return null; + } + + return [ + $methodNameArg->value => [ + $classReflection, + $methodNameArg->value, + $attribute->getLine(), + ], + ]; + } + } diff --git a/src/Rules/PHPUnit/DataProviderHelperFactory.php b/src/Rules/PHPUnit/DataProviderHelperFactory.php index a93ecfd3..7fc8af0f 100644 --- a/src/Rules/PHPUnit/DataProviderHelperFactory.php +++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\FileTypeMapper; use PHPUnit\Framework\TestCase; use function dirname; use function explode; @@ -16,9 +17,13 @@ class DataProviderHelperFactory /** @var ReflectionProvider */ private $reflectionProvider; - public function __construct(ReflectionProvider $reflectionProvider) + /** @var FileTypeMapper */ + private $fileTypeMapper; + + public function __construct(ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper) { $this->reflectionProvider = $reflectionProvider; + $this->fileTypeMapper = $fileTypeMapper; } public function create(): DataProviderHelper @@ -46,7 +51,7 @@ public function create(): DataProviderHelper } } - return new DataProviderHelper($this->reflectionProvider, $phpUnit10OrNewer); + return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $phpUnit10OrNewer); } } diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index 03c44ecb..18cc12b3 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -17,8 +17,7 @@ protected function getRule(): Rule $reflection = $this->createReflectionProvider(); return new DataProviderDeclarationRule( - new DataProviderHelper($reflection, true), - self::getContainer()->getByType(FileTypeMapper::class), + new DataProviderHelper($reflection, self::getContainer()->getByType(FileTypeMapper::class),true), true, true ); @@ -29,23 +28,39 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [ [ '@dataProvider providebaz related method is used with incorrect case: provideBaz.', - 14, + 16, ], [ '@dataProvider provideQux related method must be static in PHPUnit 10 and newer.', - 14, + 16, ], [ '@dataProvider provideQuux related method must be public.', - 14, + 16, ], [ '@dataProvider provideNonExisting related method not found.', - 68, + 70, ], [ '@dataProvider NonExisting::provideNonExisting related class not found.', - 68, + 70, + ], + [ + '@dataProvider provideNonExisting related method not found.', + 85, + ], + [ + '@dataProvider provideNonExisting2 related method not found.', + 86, + ], + [ + '@dataProvider ExampleTestCase\\BarTestCase::providetootherclass related method is used with incorrect case: provideToOtherClass.', + 87, + ], + [ + '@dataProvider ExampleTestCase\\BarTestCase::providetootherclass related method is used with incorrect case: provideToOtherClass.', + 88, ], ]); } diff --git a/tests/Rules/PHPUnit/data/data-provider-declaration.php b/tests/Rules/PHPUnit/data/data-provider-declaration.php index 23af826b..176be2dc 100644 --- a/tests/Rules/PHPUnit/data/data-provider-declaration.php +++ b/tests/Rules/PHPUnit/data/data-provider-declaration.php @@ -2,6 +2,8 @@ namespace ExampleTestCase; +use \PHPUnit\Framework\Attributes\DataProvider; + class FooTestCase extends \PHPUnit\Framework\TestCase { /** @@ -77,3 +79,15 @@ public static function provideToOtherClass(): iterable ]; } } + +class BazTestCase extends \PHPUnit\Framework\TestCase +{ + #[\PHPUnit\Framework\Attributes\DataProvider('provideNonExisting')] + #[DataProvider('provideNonExisting2')] + #[\PHPUnit\Framework\Attributes\DataProviderExternal('\\ExampleTestCase\\BarTestCase', 'providetootherclass')] + #[\PHPUnit\Framework\Attributes\DataProviderExternal(\ExampleTestCase\BarTestCase::class, 'providetootherclass')] + public function testIsNotBaz(string $subject): void + { + self::assertNotSame('baz', $subject); + } +} From bf47c49afb7dc6e897d1f3c9b61b4bd82cc4d6e6 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Wed, 21 Dec 2022 17:11:49 +0100 Subject: [PATCH 176/277] Ease the usage of AssertRuleHelper::isMethodOrStaticCallOnAssert() --- src/Rules/PHPUnit/AssertRuleHelper.php | 3 +++ src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 5 ----- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 5 ----- src/Rules/PHPUnit/AssertSameWithCountRule.php | 5 ----- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index 96735379..8c288b2c 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -11,6 +11,9 @@ class AssertRuleHelper { + /** + * @phpstan-assert-if-true Node\Expr\MethodCall|Node\Expr\StaticCall $node + */ public static function isMethodOrStaticCallOnAssert(Node $node, Scope $scope): bool { $testCaseType = new ObjectType('PHPUnit\Framework\Assert'); diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 284f53b9..6a8eb7dd 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -4,8 +4,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; @@ -28,9 +26,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var MethodCall|StaticCall $node */ - $node = $node; - if (count($node->getArgs()) < 2) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index ca8ac607..10302233 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -4,8 +4,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; @@ -28,9 +26,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var MethodCall|StaticCall $node */ - $node = $node; - if (count($node->getArgs()) < 2) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index a0a79a96..876dd87b 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -4,8 +4,6 @@ use Countable; use PhpParser\Node; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; @@ -29,9 +27,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var MethodCall|StaticCall $node */ - $node = $node; - if (count($node->getArgs()) < 2) { return []; } From 87516ff05e173ca19a4c0f13c1e6324600c3820e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Jan 2023 15:49:24 +0100 Subject: [PATCH 177/277] Require PHPStan 1.10 --- composer.json | 2 +- tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 1 + tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f3ce0e7f..594629fc 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.3" + "phpstan/phpstan": "^1.10" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 87e5a3af..08986404 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -43,6 +43,7 @@ public function testRule(): void [ 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', 39, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method PHPUnit\Framework\Assert::assertSame() with 1 and 1 will always evaluate to true.', diff --git a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php index 700f420d..4c065466 100644 --- a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php @@ -37,6 +37,7 @@ public function testBug141(): void [ "Call to method PHPUnit\Framework\Assert::assertEmpty() with non-empty-array<'0.6.0'|'1.0.0'|'1.0.x-dev'|'1.1.x-dev'|'9999999-dev'|'dev-feature-b', true> will always evaluate to false.", 23, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]); } From 75f87d4911377ad36aef67de528dc3dcaa9df77c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 31 Jan 2023 16:54:04 +0100 Subject: [PATCH 178/277] Fix build --- .../MockObjectTypeNodeResolverExtension.php | 7 +- src/Rules/PHPUnit/MockMethodCallRule.php | 74 ++++++++++--------- .../MockObjectDynamicReturnTypeExtension.php | 8 +- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php index 4d463799..2d70b380 100644 --- a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -11,8 +11,8 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeWithClassName; use function array_key_exists; +use function count; class MockObjectTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension { @@ -44,11 +44,12 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type $types = $this->typeNodeResolver->resolveMultiple($typeNode->types, $nameScope); foreach ($types as $type) { - if (!$type instanceof TypeWithClassName) { + $classNames = $type->getObjectClassNames(); + if (count($classNames) !== 1) { continue; } - if (array_key_exists($type->getClassName(), $mockClassNames)) { + if (array_key_exists($classNames[0], $mockClassNames)) { $resultType = TypeCombinator::intersect(...$types); if ($resultType instanceof NeverType) { continue; diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index ae554e1f..ab677603 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; @@ -44,53 +43,56 @@ public function processNode(Node $node, Scope $scope): array } $argType = $scope->getType($node->getArgs()[0]->value); - if (!($argType instanceof ConstantStringType)) { + if (count($argType->getConstantStrings()) === 0) { return []; } - $method = $argType->getValue(); - $type = $scope->getType($node->var); - - if ( - $type instanceof IntersectionType - && ( - in_array(MockObject::class, $type->getReferencedClasses(), true) - || in_array(Stub::class, $type->getReferencedClasses(), true) - ) - && !$type->hasMethod($method)->yes() - ) { - $mockClass = array_filter($type->getReferencedClasses(), static function (string $class): bool { - return $class !== MockObject::class && $class !== Stub::class; - }); - - return [ - sprintf( + $errors = []; + foreach ($argType->getConstantStrings() as $constantString) { + $method = $constantString->getValue(); + $type = $scope->getType($node->var); + + if ( + $type instanceof IntersectionType + && ( + in_array(MockObject::class, $type->getObjectClassNames(), true) + || in_array(Stub::class, $type->getObjectClassNames(), true) + ) + && !$type->hasMethod($method)->yes() + ) { + $mockClass = array_filter($type->getObjectClassNames(), static function (string $class): bool { + return $class !== MockObject::class && $class !== Stub::class; + }); + + $errors[] = sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, implode('&', $mockClass) - ), - ]; - } + ); + } + + if ( + !($type instanceof GenericObjectType) + || $type->getClassName() !== InvocationMocker::class + || count($type->getTypes()) <= 0 + ) { + continue; + } - if ( - $type instanceof GenericObjectType - && $type->getClassName() === InvocationMocker::class - && count($type->getTypes()) > 0 - ) { $mockClass = $type->getTypes()[0]; - if ($mockClass instanceof ObjectType && !$mockClass->hasMethod($method)->yes()) { - return [ - sprintf( - 'Trying to mock an undefined method %s() on class %s.', - $method, - $mockClass->getClassName() - ), - ]; + if (!($mockClass instanceof ObjectType) || $mockClass->hasMethod($method)->yes()) { + continue; } + + $errors[] = sprintf( + 'Trying to mock an undefined method %s() on class %s.', + $method, + $mockClass->getClassName() + ); } - return []; + return $errors; } } diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php index cb0b3a17..6db3a73b 100644 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -10,7 +10,6 @@ use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeWithClassName; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; use function array_filter; @@ -38,7 +37,12 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } $mockClasses = array_values(array_filter($type->getTypes(), static function (Type $type): bool { - return !$type instanceof TypeWithClassName || $type->getClassName() !== MockObject::class; + $classNames = $type->getObjectClassNames(); + if (count($classNames) !== 1) { + return true; + } + + return $classNames[0] !== MockObject::class; })); if (count($mockClasses) !== 1) { From d77af96c1aaec28f7c0293677132eaaad079e01b Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Wed, 8 Feb 2023 16:39:45 +0000 Subject: [PATCH 179/277] Fix empty @covers annotation causing a crash. --- src/Rules/PHPUnit/CoversHelper.php | 6 ++++++ tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php | 4 ++++ tests/Rules/PHPUnit/data/class-coverage.php | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index 8bcf92e4..a96257f0 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -95,6 +95,12 @@ public function processCovers( ))->build(); } } else { + if ($covers === '') { + $errors[] = RuleErrorBuilder::message('@covers value does not specify anything.')->build(); + + return $errors; + } + if (!isset($method) && $this->reflectionProvider->hasFunction(new Name($covers, []), null)) { return $errors; } diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php index 7ead3a7c..7b84b50f 100644 --- a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -36,6 +36,10 @@ public function testRule(): void '@covers value \Not\A\Class references an invalid class or function.', 31, ], + [ + '@covers value does not specify anything.', + 43, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index fca878f2..a5ddd180 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -36,3 +36,10 @@ function testable(): void { } + +/** + * @covers + */ +class CoversNothing extends \PHPUnit\Framework\TestCase +{ +} From db436df51b0de4301fd1b522f9c7aa455d8427b8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 18 Feb 2023 14:43:06 +0100 Subject: [PATCH 180/277] Do not use `instanceof *Type` --- src/Rules/PHPUnit/MockMethodCallRule.php | 17 +++-------------- .../AssertTypeSpecifyingExtensionHelper.php | 14 +++++++------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index ab677603..79da2d93 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -6,9 +6,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntersectionType; -use PHPStan\Type\ObjectType; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\Stub; @@ -71,24 +69,15 @@ public function processNode(Node $node, Scope $scope): array ); } - if ( - !($type instanceof GenericObjectType) - || $type->getClassName() !== InvocationMocker::class - || count($type->getTypes()) <= 0 - ) { - continue; - } - - $mockClass = $type->getTypes()[0]; - - if (!($mockClass instanceof ObjectType) || $mockClass->hasMethod($method)->yes()) { + $mockedClassObject = $type->getTemplateType(InvocationMocker::class, 'TMockedClass'); + if ($mockedClassObject->hasMethod($method)->yes()) { continue; } $errors[] = sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - $mockClass->getClassName() + implode('|', $mockedClassObject->getObjectClassNames()) ); } diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index e2684062..fbed91e7 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -18,7 +18,6 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; -use PHPStan\Type\Constant\ConstantStringType; use ReflectionObject; use function array_key_exists; use function count; @@ -125,14 +124,15 @@ private static function getExpressionResolvers(): array if (self::$resolvers === null) { self::$resolvers = [ 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { - $classType = $scope->getType($class->value); - if (!$classType instanceof ConstantStringType) { + $classType = $scope->getType($class->value)->getClassStringObjectType(); + $classNames = $classType->getObjectClassNames(); + if (count($classNames) !== 1) { return null; } return new Instanceof_( $object->value, - new Name($classType->getValue()) + new Name($classNames[0]) ); }, 'Same' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { @@ -205,12 +205,12 @@ private static function getExpressionResolvers(): array return new FuncCall(new Name('is_scalar'), [$actual]); }, 'InternalType' => static function (Scope $scope, Arg $type, Arg $value): ?FuncCall { - $typeType = $scope->getType($type->value); - if (!$typeType instanceof ConstantStringType) { + $typeNames = $scope->getType($type->value)->getConstantStrings(); + if (count($typeNames) !== 1) { return null; } - switch ($typeType->getValue()) { + switch ($typeNames[0]->getValue()) { case 'numeric': $functionName = 'is_numeric'; break; From abc2da969a8a28f22365cabfebe8b2c3ea5fabb8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 19 Feb 2023 12:12:42 +0100 Subject: [PATCH 181/277] Fix build --- src/Rules/PHPUnit/MockMethodCallRule.php | 4 +--- .../MockObjectDynamicReturnTypeExtension.php | 16 +++------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index 79da2d93..107b4ea8 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\IntersectionType; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\Stub; @@ -51,8 +50,7 @@ public function processNode(Node $node, Scope $scope): array $type = $scope->getType($node->var); if ( - $type instanceof IntersectionType - && ( + ( in_array(MockObject::class, $type->getObjectClassNames(), true) || in_array(Stub::class, $type->getObjectClassNames(), true) ) diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php index 6db3a73b..4f74fe68 100644 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Generic\GenericObjectType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; @@ -32,24 +31,15 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { $type = $scope->getType($methodCall->var); - if (!($type instanceof IntersectionType)) { - return new ObjectType(InvocationMocker::class); - } - - $mockClasses = array_values(array_filter($type->getTypes(), static function (Type $type): bool { - $classNames = $type->getObjectClassNames(); - if (count($classNames) !== 1) { - return true; - } - - return $classNames[0] !== MockObject::class; + $mockClasses = array_values(array_filter($type->getObjectClassNames(), static function (string $class): bool { + return $class !== MockObject::class; })); if (count($mockClasses) !== 1) { return new ObjectType(InvocationMocker::class); } - return new GenericObjectType(InvocationMocker::class, $mockClasses); + return new GenericObjectType(InvocationMocker::class, [new ObjectType($mockClasses[0])]); } } From 4b17a2352dd70f34bc80e4fb6147609598dd4617 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 21 Feb 2023 17:54:35 +0100 Subject: [PATCH 182/277] Fix MockMethodCallRule --- src/Rules/PHPUnit/MockMethodCallRule.php | 8 ++++++-- tests/Rules/PHPUnit/data/mock-method-call.php | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index 107b4ea8..cac776c2 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -56,15 +56,19 @@ public function processNode(Node $node, Scope $scope): array ) && !$type->hasMethod($method)->yes() ) { - $mockClass = array_filter($type->getObjectClassNames(), static function (string $class): bool { + $mockClasses = array_filter($type->getObjectClassNames(), static function (string $class): bool { return $class !== MockObject::class && $class !== Stub::class; }); + if (count($mockClasses) === 0) { + continue; + } $errors[] = sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - implode('&', $mockClass) + implode('&', $mockClasses) ); + continue; } $mockedClassObject = $type->getTemplateType(InvocationMocker::class, 'TMockedClass'); diff --git a/tests/Rules/PHPUnit/data/mock-method-call.php b/tests/Rules/PHPUnit/data/mock-method-call.php index bf0fd053..478fa443 100644 --- a/tests/Rules/PHPUnit/data/mock-method-call.php +++ b/tests/Rules/PHPUnit/data/mock-method-call.php @@ -36,6 +36,11 @@ public function testBadMethodOnStub() $this->createStub(Bar::class)->method('doBadThing'); } + public function testMockObject(\PHPUnit\Framework\MockObject\MockObject $mock) + { + $mock->method('doFoo'); + } + } class Bar { From 7e43c8f77c7e419730ead01c8dc787c6bcbe0e15 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 21 Feb 2023 19:39:03 +0100 Subject: [PATCH 183/277] Fix handling assertInstanceOf --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 6 +++--- .../Rules/PHPUnit/data/impossible-assert-method-call.php | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index fbed91e7..2276b51b 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -124,15 +124,15 @@ private static function getExpressionResolvers(): array if (self::$resolvers === null) { self::$resolvers = [ 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { - $classType = $scope->getType($class->value)->getClassStringObjectType(); - $classNames = $classType->getObjectClassNames(); + $classType = $scope->getType($class->value); + $classNames = $classType->getConstantStrings(); if (count($classNames) !== 1) { return null; } return new Instanceof_( $object->value, - new Name($classNames[0]) + new Name($classNames[0]->getValue()) ); }, 'Same' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { diff --git a/tests/Rules/PHPUnit/data/impossible-assert-method-call.php b/tests/Rules/PHPUnit/data/impossible-assert-method-call.php index a406f1e5..8b996725 100644 --- a/tests/Rules/PHPUnit/data/impossible-assert-method-call.php +++ b/tests/Rules/PHPUnit/data/impossible-assert-method-call.php @@ -20,4 +20,13 @@ public function doBar(object $o): void $this->assertEmpty($o); } + /** + * @param class-string<\Exception> $name + * @return void + */ + public function doBaz(\Exception $e, string $name): void + { + $this->assertInstanceOf($name, $e); + } + } From 4a19a3cb5b2d28b143f350e45e9f6e17e2cb81b5 Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Thu, 23 Feb 2023 12:55:09 +0000 Subject: [PATCH 184/277] Add tip to error when a not fully qualified name is seen in @covers annotation. --- src/Rules/PHPUnit/CoversHelper.php | 10 ++++++++-- tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php | 5 +++++ tests/Rules/PHPUnit/data/class-coverage.php | 7 +++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index a96257f0..792fcc11 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -105,11 +105,17 @@ public function processCovers( return $errors; } - $errors[] = RuleErrorBuilder::message(sprintf( + $error = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid %s.', $fullName, $isMethod ? 'method' : 'class or function' - ))->build(); + )); + + if (strpos($className, '\\') === false) { + $error->tip('The @covers annotation requires a fully qualified name.'); + } + + $errors[] = $error->build(); } return $errors; } diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php index 7b84b50f..32806edc 100644 --- a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -40,6 +40,11 @@ public function testRule(): void '@covers value does not specify anything.', 43, ], + [ + '@covers value NotFullyQualified references an invalid class or function.', + 50, + 'The @covers annotation requires a fully qualified name.', + ], ]); } diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index a5ddd180..35af2049 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -43,3 +43,10 @@ function testable(): void class CoversNothing extends \PHPUnit\Framework\TestCase { } + +/** + * @covers NotFullyQualified + */ +class CoversNotFullyQualified extends \PHPUnit\Framework\TestCase +{ +} From 34ee324a2b8fcab680fbb3f3f3d6c86389df35ba Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Tue, 28 Feb 2023 11:18:13 +0000 Subject: [PATCH 185/277] Fixed false positive when covering a global function. --- src/Rules/PHPUnit/CoversHelper.php | 22 +++++++++++---------- tests/Rules/PHPUnit/data/class-coverage.php | 7 +++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index 792fcc11..66d6edf8 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -71,6 +71,13 @@ public function processCovers( { $errors = []; $covers = (string) $phpDocTag->value; + + if ($covers === '') { + $errors[] = RuleErrorBuilder::message('@covers value does not specify anything.')->build(); + + return $errors; + } + $isMethod = strpos($covers, '::') !== false; $fullName = $covers; @@ -94,17 +101,12 @@ public function processCovers( $fullName ))->build(); } - } else { - if ($covers === '') { - $errors[] = RuleErrorBuilder::message('@covers value does not specify anything.')->build(); - - return $errors; - } - - if (!isset($method) && $this->reflectionProvider->hasFunction(new Name($covers, []), null)) { - return $errors; - } + } elseif (isset($method) && $this->reflectionProvider->hasFunction(new Name($method, []), null)) { + return $errors; + } elseif (!isset($method) && $this->reflectionProvider->hasFunction(new Name($className, []), null)) { + return $errors; + } else { $error = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid %s.', $fullName, diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index 35af2049..2d4e0ef8 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -50,3 +50,10 @@ class CoversNothing extends \PHPUnit\Framework\TestCase class CoversNotFullyQualified extends \PHPUnit\Framework\TestCase { } + +/** + * @covers ::str_replace + */ +class CoversGlobalFunction extends \PHPUnit\Framework\TestCase +{ +} From 4cc5c6cc38e56bce7ea47c4091814e516d172dc3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 2 Mar 2023 11:22:40 +0100 Subject: [PATCH 186/277] MockMethodCallRule - do not report for empty `$mockClasses` --- src/Rules/PHPUnit/MockMethodCallRule.php | 7 ++++++- tests/Rules/PHPUnit/data/mock-method-call.php | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index cac776c2..da8a95d7 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -76,10 +76,15 @@ public function processNode(Node $node, Scope $scope): array continue; } + $classNames = $mockedClassObject->getObjectClassNames(); + if (count($classNames) === 0) { + continue; + } + $errors[] = sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - implode('|', $mockedClassObject->getObjectClassNames()) + implode('|', $classNames) ); } diff --git a/tests/Rules/PHPUnit/data/mock-method-call.php b/tests/Rules/PHPUnit/data/mock-method-call.php index 478fa443..a4f5aaae 100644 --- a/tests/Rules/PHPUnit/data/mock-method-call.php +++ b/tests/Rules/PHPUnit/data/mock-method-call.php @@ -56,3 +56,18 @@ public function method(string $string) return $string; } }; + +final class FinalFoo +{ + +} + +class FinalFooTest extends \PHPUnit\Framework\TestCase +{ + + public function testMockFinalClass() + { + $this->createMock(FinalFoo::class)->method('doFoo'); + } + +} From f6592437762938425a51e6613bbac649c0bd7daa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 02:45:43 +0000 Subject: [PATCH 187/277] Update metcalfc/changelog-generator action to v4.1.0 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bac4a006..92b72547 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.0.1 + uses: metcalfc/changelog-generator@v4.1.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From ceea85eb8c43e5ae22040c18d3f360ca9ad5dc4f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 22 Mar 2023 09:36:13 +0100 Subject: [PATCH 188/277] Update PHPCS --- build-cs/composer.json | 7 +-- build-cs/composer.lock | 117 ++++++++++++++++++++++------------------- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/build-cs/composer.json b/build-cs/composer.json index e3079710..16a240bc 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -1,8 +1,9 @@ { "require-dev": { - "consistence-community/coding-standard": "^3.10", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^7.0" + "consistence-community/coding-standard": "^3.11.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "slevomat/coding-standard": "^8.8.0", + "squizlabs/php_codesniffer": "^3.5.3" }, "config": { "allow-plugins": { diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 4bcc8de4..c25a151a 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -4,35 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4485bbedba7bcc71ace5f69dbb9b6c47", + "content-hash": "e69c1916405a7e3c8001c1b609a0ee61", "packages": [], "packages-dev": [ { "name": "consistence-community/coding-standard", - "version": "3.11.1", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df" + "reference": "adb4be482e76990552bf624309d2acc8754ba1bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/4632fead8c9ee8f50044fcbce9f66c797b34c0df", - "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/adb4be482e76990552bf624309d2acc8754ba1bd", + "reference": "adb4be482e76990552bf624309d2acc8754ba1bd", "shasum": "" }, "require": { - "php": ">=7.4", - "slevomat/coding-standard": "~7.0", - "squizlabs/php_codesniffer": "~3.6.0" + "php": "~8.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "~3.7.0" }, "replace": { "consistence/coding-standard": "3.10.*" }, "require-dev": { - "phing/phing": "2.16.4", - "php-parallel-lint/php-parallel-lint": "1.3.0", - "phpunit/phpunit": "9.5.4" + "phing/phing": "2.17.0", + "php-parallel-lint/php-parallel-lint": "1.3.1", + "phpunit/phpunit": "9.5.10" }, "type": "library", "autoload": { @@ -70,41 +70,44 @@ ], "support": { "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.1" + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.2" }, - "time": "2021-05-03T18:13:22+00:00" + "time": "2022-06-21T08:36:36+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.2", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", + "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -120,7 +123,7 @@ }, { "name": "Contributors", - "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -144,23 +147,23 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2022-02-04T12:51:07+00:00" + "time": "2023-01-05T11:28:13+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.5.1", + "version": "1.15.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "981cc368a216c988e862a75e526b6076987d1b50" + "reference": "61800f71a5526081d1b5633766aa88341f1ade76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/981cc368a216c988e862a75e526b6076987d1b50", - "reference": "981cc368a216c988e862a75e526b6076987d1b50", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/61800f71a5526081d1b5633766aa88341f1ade76", + "reference": "61800f71a5526081d1b5633766aa88341f1ade76", "shasum": "" }, "require": { @@ -170,6 +173,7 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" @@ -189,43 +193,43 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.5.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.15.3" }, - "time": "2022-05-05T11:32:40+00:00" + "time": "2022-12-20T20:56:55+00:00" }, { "name": "slevomat/coding-standard", - "version": "7.2.1", + "version": "8.8.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90" + "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/aff06ae7a84e4534bf6f821dc982a93a5d477c90", - "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/59e25146a4ef0a7b194c5bc55b32dd414345db89", + "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.5.1", - "squizlabs/php_codesniffer": "^3.6.2" + "phpstan/phpdoc-parser": ">=1.15.2 <1.16.0", + "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { - "phing/phing": "2.17.3", + "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.7.1", - "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.1.1", - "phpstan/phpstan-strict-rules": "1.2.3", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.20" + "phpstan/phpstan": "1.4.10|1.9.6", + "phpstan/phpstan-deprecation-rules": "1.1.1", + "phpstan/phpstan-phpunit": "1.0.0|1.3.3", + "phpstan/phpstan-strict-rules": "1.4.4", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.27" }, "type": "phpcodesniffer-standard", "extra": { "branch-alias": { - "dev-master": "7.x-dev" + "dev-master": "8.x-dev" } }, "autoload": { @@ -238,9 +242,13 @@ "MIT" ], "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.2.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.8.0" }, "funding": [ { @@ -252,20 +260,20 @@ "type": "tidelift" } ], - "time": "2022-05-25T10:58:12+00:00" + "time": "2023-01-09T10:46:13+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -301,14 +309,15 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2021-12-12T21:44:58+00:00" + "time": "2023-02-22T23:07:41+00:00" } ], "aliases": [], From 9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 24 Mar 2023 16:09:26 +0000 Subject: [PATCH 189/277] Warn when trying to cover an interface. --- src/Rules/PHPUnit/CoversHelper.php | 8 +++++++- tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php | 4 ++++ tests/Rules/PHPUnit/data/class-coverage.php | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index 66d6edf8..f17a2063 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -95,6 +95,13 @@ public function processCovers( if ($this->reflectionProvider->hasClass($className)) { $class = $this->reflectionProvider->getClass($className); + if ($class->isInterface()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@covers value %s references an interface.', + $fullName + ))->build(); + } + if (isset($method) && $method !== '' && !$class->hasMethod($method)) { $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid method.', @@ -105,7 +112,6 @@ public function processCovers( return $errors; } elseif (!isset($method) && $this->reflectionProvider->hasFunction(new Name($className, []), null)) { return $errors; - } else { $error = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid %s.', diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php index 32806edc..69a7de2f 100644 --- a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -45,6 +45,10 @@ public function testRule(): void 50, 'The @covers annotation requires a fully qualified name.', ], + [ + '@covers value \DateTimeInterface references an interface.', + 64, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index 2d4e0ef8..c231f772 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -57,3 +57,10 @@ class CoversNotFullyQualified extends \PHPUnit\Framework\TestCase class CoversGlobalFunction extends \PHPUnit\Framework\TestCase { } + +/** + * @covers \DateTimeInterface + */ +class CoversInterface extends \PHPUnit\Framework\TestCase +{ +} From 8b82ef2c494ab779f8d5230036b28c99622e881c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 02:12:31 +0000 Subject: [PATCH 190/277] Update dependency slevomat/coding-standard to v8.9.0 --- build-cs/composer.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index c25a151a..b52da6f8 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.15.3", + "version": "1.16.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "61800f71a5526081d1b5633766aa88341f1ade76" + "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/61800f71a5526081d1b5633766aa88341f1ade76", - "reference": "61800f71a5526081d1b5633766aa88341f1ade76", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e27e92d939e2e3636f0a1f0afaba59692c0bf571", + "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571", "shasum": "" }, "require": { @@ -193,38 +193,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.15.3" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.16.1" }, - "time": "2022-12-20T20:56:55+00:00" + "time": "2023-02-07T18:11:17+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.8.0", + "version": "8.9.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89" + "reference": "8f11e0f5ff984d6862bb9d83aa513dc05a1773ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/59e25146a4ef0a7b194c5bc55b32dd414345db89", - "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/8f11e0f5ff984d6862bb9d83aa513dc05a1773ef", + "reference": "8f11e0f5ff984d6862bb9d83aa513dc05a1773ef", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.15.2 <1.16.0", + "phpstan/phpdoc-parser": ">=1.16.0 <1.17.0", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.9.6", - "phpstan/phpstan-deprecation-rules": "1.1.1", - "phpstan/phpstan-phpunit": "1.0.0|1.3.3", - "phpstan/phpstan-strict-rules": "1.4.4", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.27" + "phpstan/phpstan": "1.4.10|1.10.8", + "phpstan/phpstan-deprecation-rules": "1.1.3", + "phpstan/phpstan-phpunit": "1.0.0|1.3.10", + "phpstan/phpstan-strict-rules": "1.5.0", + "phpunit/phpunit": "7.5.20|8.5.21|9.6.5" }, "type": "phpcodesniffer-standard", "extra": { @@ -234,7 +234,7 @@ }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.8.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.9.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-01-09T10:46:13+00:00" + "time": "2023-03-25T15:52:37+00:00" }, { "name": "squizlabs/php_codesniffer", From 4ae9e7c2c106c8aa420e5768088bcdefb7e63090 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 00:11:11 +0000 Subject: [PATCH 191/277] Update build-cs --- build-cs/composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index b52da6f8..d8730d8a 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -9,16 +9,16 @@ "packages-dev": [ { "name": "consistence-community/coding-standard", - "version": "3.11.2", + "version": "3.11.3", "source": { "type": "git", "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "adb4be482e76990552bf624309d2acc8754ba1bd" + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/adb4be482e76990552bf624309d2acc8754ba1bd", - "reference": "adb4be482e76990552bf624309d2acc8754ba1bd", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", "shasum": "" }, "require": { @@ -70,9 +70,9 @@ ], "support": { "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.2" + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.3" }, - "time": "2022-06-21T08:36:36+00:00" + "time": "2023-03-27T14:55:41+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -199,16 +199,16 @@ }, { "name": "slevomat/coding-standard", - "version": "8.9.0", + "version": "8.9.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "8f11e0f5ff984d6862bb9d83aa513dc05a1773ef" + "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/8f11e0f5ff984d6862bb9d83aa513dc05a1773ef", - "reference": "8f11e0f5ff984d6862bb9d83aa513dc05a1773ef", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", + "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", "shasum": "" }, "require": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.9.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.9.1" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-03-25T15:52:37+00:00" + "time": "2023-03-27T11:00:16+00:00" }, { "name": "squizlabs/php_codesniffer", From 2365fd0df5d6c3457b30c9a0b919fd1e406f3fde Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 08:36:38 +0000 Subject: [PATCH 192/277] Update dependency slevomat/coding-standard to v8.10.0 --- build-cs/composer.lock | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index d8730d8a..2eafc159 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.16.1", + "version": "1.18.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571" + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e27e92d939e2e3636f0a1f0afaba59692c0bf571", - "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", "shasum": "" }, "require": { @@ -193,38 +193,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.16.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" }, - "time": "2023-02-07T18:11:17+00:00" + "time": "2023-04-07T11:51:11+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.9.1", + "version": "8.10.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2" + "reference": "c4e213e6e57f741451a08e68ef838802eec92287" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", - "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/c4e213e6e57f741451a08e68ef838802eec92287", + "reference": "c4e213e6e57f741451a08e68ef838802eec92287", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.16.0 <1.17.0", + "phpstan/phpdoc-parser": ">=1.18.0 <1.19.0", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.10.8", + "phpstan/phpstan": "1.4.10|1.10.11", "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.0.0|1.3.10", - "phpstan/phpstan-strict-rules": "1.5.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.5" + "phpstan/phpstan-phpunit": "1.0.0|1.3.11", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.0.19" }, "type": "phpcodesniffer-standard", "extra": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.9.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.10.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-03-27T11:00:16+00:00" + "time": "2023-04-10T07:39:29+00:00" }, { "name": "squizlabs/php_codesniffer", From c006a382a334ed01d68ad4422696c94b241c46b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 01:07:57 +0000 Subject: [PATCH 193/277] Update dependency slevomat/coding-standard to v8.11.0 --- build-cs/composer.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 2eafc159..0cea3f9e 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.18.1", + "version": "1.20.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" + "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/90490bd8fd8530a272043c4950c180b6d0cf5f81", + "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81", "shasum": "" }, "require": { @@ -193,38 +193,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.2" }, - "time": "2023-04-07T11:51:11+00:00" + "time": "2023-04-22T12:59:35+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.10.0", + "version": "8.11.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "c4e213e6e57f741451a08e68ef838802eec92287" + "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/c4e213e6e57f741451a08e68ef838802eec92287", - "reference": "c4e213e6e57f741451a08e68ef838802eec92287", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/91428d5bcf7db93a842bcf97f465edf62527f3ea", + "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.18.0 <1.19.0", + "phpstan/phpdoc-parser": ">=1.20.0 <1.21.0", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.10.11", + "phpstan/phpstan": "1.10.14", "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.0.0|1.3.11", + "phpstan/phpstan-phpunit": "1.3.11", "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.0.19" + "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.1.1" }, "type": "phpcodesniffer-standard", "extra": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.10.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.11.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-04-10T07:39:29+00:00" + "time": "2023-04-21T15:51:44+00:00" }, { "name": "squizlabs/php_codesniffer", From 9975b981899246fe7e289cf200deafcf2fefa649 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 00:53:26 +0000 Subject: [PATCH 194/277] Update dependency slevomat/coding-standard to v8.11.1 --- build-cs/composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 0cea3f9e..dab6b094 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.20.2", + "version": "1.20.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81" + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/90490bd8fd8530a272043c4950c180b6d0cf5f81", - "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", "shasum": "" }, "require": { @@ -193,22 +193,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.4" }, - "time": "2023-04-22T12:59:35+00:00" + "time": "2023-05-02T09:19:37+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.11.0", + "version": "8.11.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea" + "reference": "af87461316b257e46e15bb041dca6fca3796d822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/91428d5bcf7db93a842bcf97f465edf62527f3ea", - "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/af87461316b257e46e15bb041dca6fca3796d822", + "reference": "af87461316b257e46e15bb041dca6fca3796d822", "shasum": "" }, "require": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.11.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.11.1" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:51:44+00:00" + "time": "2023-04-24T08:19:01+00:00" }, { "name": "squizlabs/php_codesniffer", From aaf5fe559e982f1b1dabdbf9ac99ea7ce7734595 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 May 2023 02:05:35 +0000 Subject: [PATCH 195/277] Update dependency slevomat/coding-standard to v8.12.0 --- build-cs/composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index dab6b094..db122416 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -199,16 +199,16 @@ }, { "name": "slevomat/coding-standard", - "version": "8.11.1", + "version": "8.12.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "af87461316b257e46e15bb041dca6fca3796d822" + "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/af87461316b257e46e15bb041dca6fca3796d822", - "reference": "af87461316b257e46e15bb041dca6fca3796d822", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", + "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", "shasum": "" }, "require": { @@ -220,11 +220,11 @@ "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.14", + "phpstan/phpstan": "1.10.15", "phpstan/phpstan-deprecation-rules": "1.1.3", "phpstan/phpstan-phpunit": "1.3.11", "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.1.1" + "phpunit/phpunit": "7.5.20|8.5.21|9.6.8|10.1.3" }, "type": "phpcodesniffer-standard", "extra": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.11.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.12.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-04-24T08:19:01+00:00" + "time": "2023-05-14T20:06:01+00:00" }, { "name": "squizlabs/php_codesniffer", From 23009eb5269c2232689f0b690d4a2b893afcde94 Mon Sep 17 00:00:00 2001 From: Alessandro Lai Date: Mon, 22 May 2023 18:52:54 +0200 Subject: [PATCH 196/277] Add regression test for #185 --- tests/Type/PHPUnit/data/assert-function.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 117179ae..f3222f34 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -20,6 +20,17 @@ public function doFoo($o): void assertType(self::class, $o); } + /** + * @template T of object + * @param object $o + * @param class-string<\DateTimeInterface> $class + */ + public function assertInstanceOfWorksWithTemplate($o, $class): void + { + assertInstanceOf($class, $o); + assertType(\DateTimeInterface::class, $o); + } + public function arrayHasNumericKey(array $a): void { assertArrayHasKey(0, $a); assertType('array&hasOffset(0)', $a); From c44246879d692d3b2cf2a21d65be4b4715d6ef21 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 23 May 2023 13:55:04 +0200 Subject: [PATCH 197/277] Fix assertInstanceOf handler --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 2276b51b..2e8e828e 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -123,16 +123,10 @@ private static function getExpressionResolvers(): array { if (self::$resolvers === null) { self::$resolvers = [ - 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { - $classType = $scope->getType($class->value); - $classNames = $classType->getConstantStrings(); - if (count($classNames) !== 1) { - return null; - } - + 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): Instanceof_ { return new Instanceof_( $object->value, - new Name($classNames[0]->getValue()) + $class->value ); }, 'Same' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { From 9e928dd88023db780756e117b70383322247dd32 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 25 May 2023 15:27:48 +0200 Subject: [PATCH 198/277] Next-gen coding standard workflow --- .gitattributes | 2 - .github/renovate.json | 5 - .github/workflows/build.yml | 14 +- .gitignore | 1 + Makefile | 10 +- build-cs/.gitignore | 1 - build-cs/composer.json | 13 -- build-cs/composer.lock | 331 ------------------------------------ phpcs.xml | 111 ------------ 9 files changed, 21 insertions(+), 467 deletions(-) delete mode 100644 build-cs/.gitignore delete mode 100644 build-cs/composer.json delete mode 100644 build-cs/composer.lock delete mode 100644 phpcs.xml diff --git a/.gitattributes b/.gitattributes index 00d6b17d..aae3fd19 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,11 +3,9 @@ *.neon linguist-language=YAML /.* export-ignore -/build-cs export-ignore /tests export-ignore /tmp export-ignore /Makefile export-ignore -/phpcs.xml export-ignore /phpstan.neon export-ignore /phpstan-baseline.neon export-ignore /phpunit.xml export-ignore diff --git a/.github/renovate.json b/.github/renovate.json index b775cc18..d3f5961e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -10,11 +10,6 @@ "enabled": true, "groupName": "root-composer" }, - { - "matchPaths": ["build-cs/**"], - "enabled": true, - "groupName": "build-cs" - }, { "matchPaths": [".github/**"], "enabled": true, diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 35254989..afe7e6ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: - name: "Lint" run: "make lint" - coding-standards: + coding-standard: name: "Coding Standard" runs-on: "ubuntu-latest" @@ -55,11 +55,17 @@ jobs: - name: "Checkout" uses: actions/checkout@v3 + - name: "Checkout build-cs" + uses: actions/checkout@v3 + with: + repository: "phpstan/build-cs" + path: "build-cs" + - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.2" - name: "Validate Composer" run: "composer validate" @@ -67,6 +73,10 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" + - name: "Install build-cs dependencies" + working-directory: "build-cs" + run: "composer install --no-interaction --no-progress" + - name: "Lint" run: "make lint" diff --git a/.gitignore b/.gitignore index 2db21315..7de9f3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /tests/tmp +/build-cs /vendor /composer.lock .phpunit.result.cache diff --git a/Makefile b/Makefile index b34d0fec..ecd8cfb2 100644 --- a/Makefile +++ b/Makefile @@ -10,13 +10,19 @@ lint: php vendor/bin/parallel-lint --colors \ src tests +.PHONY: cs-install +cs-install: + git clone https://github.com/phpstan/build-cs.git || true + git -C build-cs fetch origin && git -C build-cs reset --hard origin/main + composer install --working-dir build-cs + .PHONY: cs cs: - composer install --working-dir build-cs && php build-cs/vendor/bin/phpcs + php build-cs/vendor/bin/phpcs --standard=build-cs/phpcs.xml src tests .PHONY: cs-fix cs-fix: - php build-cs/vendor/bin/phpcbf + php build-cs/vendor/bin/phpcbf --standard=build-cs/phpcs.xml src tests .PHONY: phpstan phpstan: diff --git a/build-cs/.gitignore b/build-cs/.gitignore deleted file mode 100644 index 61ead866..00000000 --- a/build-cs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/vendor diff --git a/build-cs/composer.json b/build-cs/composer.json deleted file mode 100644 index 16a240bc..00000000 --- a/build-cs/composer.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "require-dev": { - "consistence-community/coding-standard": "^3.11.0", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", - "slevomat/coding-standard": "^8.8.0", - "squizlabs/php_codesniffer": "^3.5.3" - }, - "config": { - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true - } - } -} diff --git a/build-cs/composer.lock b/build-cs/composer.lock deleted file mode 100644 index db122416..00000000 --- a/build-cs/composer.lock +++ /dev/null @@ -1,331 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "e69c1916405a7e3c8001c1b609a0ee61", - "packages": [], - "packages-dev": [ - { - "name": "consistence-community/coding-standard", - "version": "3.11.3", - "source": { - "type": "git", - "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", - "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", - "shasum": "" - }, - "require": { - "php": "~8.0", - "slevomat/coding-standard": "~8.0", - "squizlabs/php_codesniffer": "~3.7.0" - }, - "replace": { - "consistence/coding-standard": "3.10.*" - }, - "require-dev": { - "phing/phing": "2.17.0", - "php-parallel-lint/php-parallel-lint": "1.3.1", - "phpunit/phpunit": "9.5.10" - }, - "type": "library", - "autoload": { - "psr-4": { - "Consistence\\": [ - "Consistence" - ] - }, - "classmap": [ - "Consistence" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Vašek Purchart", - "email": "me@vasekpurchart.cz", - "homepage": "http://vasekpurchart.cz" - } - ], - "description": "Consistence - Coding Standard - PHP Code Sniffer rules", - "keywords": [ - "Coding Standard", - "PHPCodeSniffer", - "codesniffer", - "coding", - "cs", - "phpcs", - "ruleset", - "sniffer", - "standard" - ], - "support": { - "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.3" - }, - "time": "2023-03-27T14:55:41+00:00" - }, - { - "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", - "source": { - "type": "git", - "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" - }, - "require-dev": { - "composer/composer": "*", - "ext-json": "*", - "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0", - "yoast/phpunit-polyfills": "^1.0" - }, - "type": "composer-plugin", - "extra": { - "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" - }, - "autoload": { - "psr-4": { - "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" - } - ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", - "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcbf", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" - ], - "support": { - "issues": "https://github.com/PHPCSStandards/composer-installer/issues", - "source": "https://github.com/PHPCSStandards/composer-installer" - }, - "time": "2023-01-05T11:28:13+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "1.20.4", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", - "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.4" - }, - "time": "2023-05-02T09:19:37+00:00" - }, - { - "name": "slevomat/coding-standard", - "version": "8.12.0", - "source": { - "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", - "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", - "shasum": "" - }, - "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", - "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.20.0 <1.21.0", - "squizlabs/php_codesniffer": "^3.7.1" - }, - "require-dev": { - "phing/phing": "2.17.4", - "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.15", - "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.3.11", - "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.8|10.1.3" - }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-master": "8.x-dev" - } - }, - "autoload": { - "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", - "keywords": [ - "dev", - "phpcs" - ], - "support": { - "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.12.0" - }, - "funding": [ - { - "url": "https://github.com/kukulich", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", - "type": "tidelift" - } - ], - "time": "2023-05-14T20:06:01+00:00" - }, - { - "name": "squizlabs/php_codesniffer", - "version": "3.7.2", - "source": { - "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" - }, - "bin": [ - "bin/phpcs", - "bin/phpcbf" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Greg Sherwood", - "role": "lead" - } - ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", - "keywords": [ - "phpcs", - "standards", - "static analysis" - ], - "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" - }, - "time": "2023-02-22T23:07:41+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.3.0" -} diff --git a/phpcs.xml b/phpcs.xml deleted file mode 100644 index 95032a6e..00000000 --- a/phpcs.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - src - tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10 - - - - - - 10 - - - - - - - - 10 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - tests/*/data - From d8bdab0218c5eb0964338d24a8511b65e9c94fa5 Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 26 May 2023 10:38:12 +0100 Subject: [PATCH 199/277] Microoptimize AssertRuleHelper. --- src/Rules/PHPUnit/AssertRuleHelper.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index 8c288b2c..3ad79c00 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -16,7 +16,6 @@ class AssertRuleHelper */ public static function isMethodOrStaticCallOnAssert(Node $node, Scope $scope): bool { - $testCaseType = new ObjectType('PHPUnit\Framework\Assert'); if ($node instanceof Node\Expr\MethodCall) { $calledOnType = $scope->getType($node->var); } elseif ($node instanceof Node\Expr\StaticCall) { @@ -45,11 +44,9 @@ public static function isMethodOrStaticCallOnAssert(Node $node, Scope $scope): b return false; } - if (!$testCaseType->isSuperTypeOf($calledOnType)->yes()) { - return false; - } + $testCaseType = new ObjectType('PHPUnit\Framework\Assert'); - return true; + return $testCaseType->isSuperTypeOf($calledOnType)->yes(); } } From d96b5a45ace45892e55927d104e5089e354040c1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 28 May 2023 17:57:41 +0200 Subject: [PATCH 200/277] Modernize rules with RuleErrorBuilder --- src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 5 +++-- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 3 ++- src/Rules/PHPUnit/AssertSameWithCountRule.php | 5 +++-- src/Rules/PHPUnit/ClassMethodCoversExistsRule.php | 1 - src/Rules/PHPUnit/MockMethodCallRule.php | 12 +++++------- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 6a8eb7dd..b2860676 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -7,6 +7,7 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use function count; /** @@ -40,13 +41,13 @@ public function processNode(Node $node, Scope $scope): array if ($expectedArgumentValue->name->toLowerString() === 'true') { return [ - 'You should use assertTrue() instead of assertSame() when expecting "true"', + RuleErrorBuilder::message('You should use assertTrue() instead of assertSame() when expecting "true"')->build(), ]; } if ($expectedArgumentValue->name->toLowerString() === 'false') { return [ - 'You should use assertFalse() instead of assertSame() when expecting "false"', + RuleErrorBuilder::message('You should use assertFalse() instead of assertSame() when expecting "false"')->build(), ]; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 10302233..cf6fc76e 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -7,6 +7,7 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use function count; /** @@ -40,7 +41,7 @@ public function processNode(Node $node, Scope $scope): array if ($expectedArgumentValue->name->toLowerString() === 'null') { return [ - 'You should use assertNull() instead of assertSame(null, $actual).', + RuleErrorBuilder::message('You should use assertNull() instead of assertSame(null, $actual).')->build(), ]; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 876dd87b..209614ae 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -7,6 +7,7 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ObjectType; use function count; @@ -42,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array && $right->name->toLowerString() === 'count' ) { return [ - 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', + RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).')->build(), ]; } @@ -56,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array if ((new ObjectType(Countable::class))->isSuperTypeOf($type)->yes()) { return [ - 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).', + RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).')->build(), ]; } } diff --git a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php index 95e6cc89..69693dee 100644 --- a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php +++ b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php @@ -62,7 +62,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - $errors = []; $classPhpDoc = $classReflection->getResolvedPhpDoc(); [$classCovers, $classCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($classPhpDoc); diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index da8a95d7..90dc0945 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\Stub; @@ -28,9 +29,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - /** @var Node\Expr\MethodCall $node */ - $node = $node; - if (!$node->name instanceof Node\Identifier || $node->name->name !== 'method') { return []; } @@ -63,11 +61,11 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = sprintf( + $errors[] = RuleErrorBuilder::message(sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, implode('&', $mockClasses) - ); + ))->build(); continue; } @@ -81,11 +79,11 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = sprintf( + $errors[] = RuleErrorBuilder::message(sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, implode('|', $classNames) - ); + ))->build(); } return $errors; From abc6e126eaccb792c2a2b6bd11760cee0b574c18 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 5 Jun 2023 10:05:44 +0200 Subject: [PATCH 201/277] Update phpstan/phpstan-strict-rules --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 594629fc..ea9f5cdf 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "require-dev": { "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.5.1", "phpunit/phpunit": "^9.5" }, "config": { From f2118dbe168588aa4a7a300c84c822a0732c485f Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Fri, 12 May 2023 17:53:05 +0200 Subject: [PATCH 202/277] Improve support of assertArrayHasKey() --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 10 ++++++++-- tests/Type/PHPUnit/data/assert-function.php | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 2e8e828e..a7edf21e 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -262,8 +262,14 @@ private static function getExpressionResolvers(): array ] ); }, - 'ArrayHasKey' => static function (Scope $scope, Arg $key, Arg $array): FuncCall { - return new FuncCall(new Name('array_key_exists'), [$key, $array]); + 'ArrayHasKey' => static function (Scope $scope, Arg $key, Arg $array): Expr { + return new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + new Expr\Instanceof_($array->value, new Name('ArrayAccess')), + new Expr\MethodCall($array->value, 'offsetExists', [$key]) + ), + new FuncCall(new Name('array_key_exists'), [$key, $array]) + ); }, 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { return new FuncCall(new Name('property_exists'), [$object, $property]); diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index f3222f34..9af1d264 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -31,15 +31,21 @@ public function assertInstanceOfWorksWithTemplate($o, $class): void assertType(\DateTimeInterface::class, $o); } - public function arrayHasNumericKey(array $a): void { + public function arrayHasNumericKey(array $a, \ArrayAccess $b): void { assertArrayHasKey(0, $a); assertType('array&hasOffset(0)', $a); + + assertArrayHasKey(0, $b); + assertType('ArrayAccess', $b); } - public function arrayHasStringKey(array $a): void + public function arrayHasStringKey(array $a, \ArrayAccess $b): void { assertArrayHasKey('key', $a); assertType("array&hasOffset('key')", $a); + + assertArrayHasKey('key', $b); + assertType("ArrayAccess", $b); } public function objectHasAttribute(object $a): void From 2742e1c0d6924ff03fc640954ee4d25143d90699 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Tue, 2 May 2023 14:30:28 +0200 Subject: [PATCH 203/277] Add partial support for assertContainsOnlyInstancesOf --- .../AssertTypeSpecifyingExtensionHelper.php | 24 +++++++++++++++++++ tests/Type/PHPUnit/data/assert-function.php | 10 ++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index a7edf21e..1507a135 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -13,7 +13,9 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Name; +use PhpParser\Node\Param; use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Stmt; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -274,6 +276,28 @@ private static function getExpressionResolvers(): array 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { return new FuncCall(new Name('property_exists'), [$object, $property]); }, + 'ContainsOnlyInstancesOf' => static function (Scope $scope, Arg $className, Arg $haystack): Expr { + return new Expr\BinaryOp\BooleanOr( + new Expr\Instanceof_($haystack->value, new Name('Traversable')), + new Identical( + $haystack->value, + new FuncCall(new Name('array_filter'), [ + $haystack, + new Arg(new Expr\Closure([ + 'static' => true, + 'params' => [ + new Param(new Expr\Variable('_')), + ], + 'stmts' => [ + new Stmt\Return_( + new FuncCall(new Name('is_a'), [new Arg(new Expr\Variable('_')), $className]) + ), + ], + ])), + ]) + ) + ); + }, ]; } diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 9af1d264..84102c97 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -4,6 +4,7 @@ use function PHPStan\Testing\assertType; use function PHPUnit\Framework\assertArrayHasKey; +use function PHPUnit\Framework\assertContainsOnlyInstancesOf; use function PHPUnit\Framework\assertEmpty; use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertObjectHasAttribute; @@ -60,4 +61,13 @@ public function testEmpty($a): void assertType("0|0.0|''|'0'|array{}|Countable|EmptyIterator|false|null", $a); } + public function containsOnlyInstancesOf(array $a, \Traversable $b): void + { + assertContainsOnlyInstancesOf(\stdClass::class, $a); + assertType('array', $a); + + assertContainsOnlyInstancesOf(\stdClass::class, $b); + assertType('Traversable', $b); + } + } From e468b76c2b89ce1611e2fc5ad3f6420dd49ddfbf Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Fri, 21 Jul 2023 15:03:05 +0200 Subject: [PATCH 204/277] Add support for assertContains and assertContainsEquals --- .../AssertTypeSpecifyingExtensionHelper.php | 15 ++++++++++++++ tests/Type/PHPUnit/data/assert-function.php | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 1507a135..34d86ca1 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -276,6 +276,21 @@ private static function getExpressionResolvers(): array 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { return new FuncCall(new Name('property_exists'), [$object, $property]); }, + 'Contains' => static function (Scope $scope, Arg $needle, Arg $haystack): Expr { + return new Expr\BinaryOp\BooleanOr( + new Expr\Instanceof_($haystack->value, new Name('Traversable')), + new FuncCall(new Name('in_array'), [$needle, $haystack, new Arg(new ConstFetch(new Name('true')))]) + ); + }, + 'ContainsEquals' => static function (Scope $scope, Arg $needle, Arg $haystack): Expr { + return new Expr\BinaryOp\BooleanOr( + new Expr\Instanceof_($haystack->value, new Name('Traversable')), + new Expr\BinaryOp\BooleanAnd( + new Expr\BooleanNot(new Expr\Empty_($haystack->value)), + new FuncCall(new Name('in_array'), [$needle, $haystack, new Arg(new ConstFetch(new Name('false')))]) + ) + ); + }, 'ContainsOnlyInstancesOf' => static function (Scope $scope, Arg $className, Arg $haystack): Expr { return new Expr\BinaryOp\BooleanOr( new Expr\Instanceof_($haystack->value, new Name('Traversable')), diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 84102c97..15c4371a 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -4,6 +4,8 @@ use function PHPStan\Testing\assertType; use function PHPUnit\Framework\assertArrayHasKey; +use function PHPUnit\Framework\assertContains; +use function PHPUnit\Framework\assertContainsEquals; use function PHPUnit\Framework\assertContainsOnlyInstancesOf; use function PHPUnit\Framework\assertEmpty; use function PHPUnit\Framework\assertInstanceOf; @@ -61,6 +63,24 @@ public function testEmpty($a): void assertType("0|0.0|''|'0'|array{}|Countable|EmptyIterator|false|null", $a); } + public function contains(array $a, \Traversable $b): void + { + assertContains('foo', $a); + assertType('non-empty-array', $a); + + assertContains('foo', $b); + assertType('Traversable', $b); + } + + public function containsEquals(array $a, \Traversable $b): void + { + assertContainsEquals('foo', $a); + assertType('non-empty-array', $a); + + assertContainsEquals('foo', $b); + assertType('Traversable', $b); + } + public function containsOnlyInstancesOf(array $a, \Traversable $b): void { assertContainsOnlyInstancesOf(\stdClass::class, $a); From 908e232dab1297e5ad6e492b5dfa2d828727915d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 28 May 2023 18:00:30 +0200 Subject: [PATCH 205/277] Error identifiers --- src/Rules/PHPUnit/AnnotationHelper.php | 6 +-- .../PHPUnit/AssertSameBooleanExpectedRule.php | 4 +- .../PHPUnit/AssertSameNullExpectedRule.php | 2 +- src/Rules/PHPUnit/AssertSameWithCountRule.php | 8 ++- src/Rules/PHPUnit/ClassCoversExistsRule.php | 14 +++--- .../PHPUnit/ClassMethodCoversExistsRule.php | 4 +- src/Rules/PHPUnit/CoversHelper.php | 14 +++--- src/Rules/PHPUnit/DataProviderHelper.php | 49 ++++++++++++------- src/Rules/PHPUnit/MockMethodCallRule.php | 4 +- .../PHPUnit/ShouldCallParentMethodsRule.php | 2 +- 10 files changed, 64 insertions(+), 43 deletions(-) diff --git a/src/Rules/PHPUnit/AnnotationHelper.php b/src/Rules/PHPUnit/AnnotationHelper.php index f5529a8f..4335bc81 100644 --- a/src/Rules/PHPUnit/AnnotationHelper.php +++ b/src/Rules/PHPUnit/AnnotationHelper.php @@ -3,7 +3,7 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Comment\Doc; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use function array_key_exists; use function in_array; @@ -30,7 +30,7 @@ class AnnotationHelper ]; /** - * @return RuleError[] errors + * @return list errors */ public function processDocComment(Doc $docComment): array { @@ -57,7 +57,7 @@ public function processDocComment(Doc $docComment): array $errors[] = RuleErrorBuilder::message( 'Annotation "' . $matches['annotation'] . '" is invalid, "@' . $matches['property'] . '" should be followed by a space and a value.' - )->build(); + )->identifier('phpunit.invalidPhpDoc')->build(); } return $errors; diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index b2860676..969f7b3c 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -41,13 +41,13 @@ public function processNode(Node $node, Scope $scope): array if ($expectedArgumentValue->name->toLowerString() === 'true') { return [ - RuleErrorBuilder::message('You should use assertTrue() instead of assertSame() when expecting "true"')->build(), + RuleErrorBuilder::message('You should use assertTrue() instead of assertSame() when expecting "true"')->identifier('phpunit.assertTrue')->build(), ]; } if ($expectedArgumentValue->name->toLowerString() === 'false') { return [ - RuleErrorBuilder::message('You should use assertFalse() instead of assertSame() when expecting "false"')->build(), + RuleErrorBuilder::message('You should use assertFalse() instead of assertSame() when expecting "false"')->identifier('phpunit.assertFalse')->build(), ]; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index cf6fc76e..a2d0cfa0 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -41,7 +41,7 @@ public function processNode(Node $node, Scope $scope): array if ($expectedArgumentValue->name->toLowerString() === 'null') { return [ - RuleErrorBuilder::message('You should use assertNull() instead of assertSame(null, $actual).')->build(), + RuleErrorBuilder::message('You should use assertNull() instead of assertSame(null, $actual).')->identifier('phpunit.assertNull')->build(), ]; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 209614ae..38fdf9ab 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -43,7 +43,9 @@ public function processNode(Node $node, Scope $scope): array && $right->name->toLowerString() === 'count' ) { return [ - RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).')->build(), + RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).') + ->identifier('phpunit.assertCount') + ->build(), ]; } @@ -57,7 +59,9 @@ public function processNode(Node $node, Scope $scope): array if ((new ObjectType(Countable::class))->isSuperTypeOf($type)->yes()) { return [ - RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).')->build(), + RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).') + ->identifier('phpunit.assertCount') + ->build(), ]; } } diff --git a/src/Rules/PHPUnit/ClassCoversExistsRule.php b/src/Rules/PHPUnit/ClassCoversExistsRule.php index a5a0cb70..bbced022 100644 --- a/src/Rules/PHPUnit/ClassCoversExistsRule.php +++ b/src/Rules/PHPUnit/ClassCoversExistsRule.php @@ -56,18 +56,18 @@ public function processNode(Node $node, Scope $scope): array return []; } - $errors = []; $classPhpDoc = $classReflection->getResolvedPhpDoc(); [$classCovers, $classCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($classPhpDoc); if (count($classCoversDefaultClasses) >= 2) { - $errors[] = RuleErrorBuilder::message(sprintf( - '@coversDefaultClass is defined multiple times.' - ))->build(); - - return $errors; + return [ + RuleErrorBuilder::message(sprintf( + '@coversDefaultClass is defined multiple times.' + ))->identifier('phpunit.coversDuplicate')->build(), + ]; } + $errors = []; $coversDefaultClass = array_shift($classCoversDefaultClasses); if ($coversDefaultClass !== null) { @@ -76,7 +76,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( '@coversDefaultClass references an invalid class %s.', $className - ))->build(); + ))->identifier('phpunit.coversClass')->build(); } } diff --git a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php index 69693dee..92cb1e2e 100644 --- a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php +++ b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php @@ -94,7 +94,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( '@coversDefaultClass defined on class method %s.', $node->name - ))->build(); + ))->identifier('phpunit.covers')->build(); } foreach ($methodCovers as $covers) { @@ -102,7 +102,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Class already @covers %s so the method @covers is redundant.', $covers->value - ))->build(); + ))->identifier('phpunit.coversDuplicate')->build(); } $errors = array_merge( diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index f17a2063..55dbe8de 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -7,7 +7,7 @@ use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use function array_merge; use function explode; @@ -61,7 +61,7 @@ public function getCoverAnnotations(?ResolvedPhpDocBlock $phpDoc): array } /** - * @return RuleError[] errors + * @return list errors */ public function processCovers( Node $node, @@ -73,7 +73,9 @@ public function processCovers( $covers = (string) $phpDocTag->value; if ($covers === '') { - $errors[] = RuleErrorBuilder::message('@covers value does not specify anything.')->build(); + $errors[] = RuleErrorBuilder::message('@covers value does not specify anything.') + ->identifier('phpunit.covers') + ->build(); return $errors; } @@ -99,14 +101,14 @@ public function processCovers( $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an interface.', $fullName - ))->build(); + ))->identifier('phpunit.coversInterface')->build(); } if (isset($method) && $method !== '' && !$class->hasMethod($method)) { $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid method.', $fullName - ))->build(); + ))->identifier('phpunit.coversMethod')->build(); } } elseif (isset($method) && $this->reflectionProvider->hasFunction(new Name($method, []), null)) { return $errors; @@ -117,7 +119,7 @@ public function processCovers( '@covers value %s references an invalid %s.', $fullName, $isMethod ? 'method' : 'class or function' - )); + ))->identifier(sprintf('phpunit.covers%s', $isMethod ? 'Method' : '')); if (strpos($className, '\\') === false) { $error->tip('The @covers annotation requires a fully qualified name.'); diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index 6651b051..9d1b160a 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -13,7 +13,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; use function array_merge; @@ -130,7 +130,7 @@ private function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array } /** - * @return RuleError[] errors + * @return list errors */ public function processDataProvider( string $dataProviderValue, @@ -142,23 +142,29 @@ public function processDataProvider( ): array { if ($classReflection === null) { - $error = RuleErrorBuilder::message(sprintf( - '@dataProvider %s related class not found.', - $dataProviderValue - ))->line($lineNumber)->build(); - - return [$error]; + return [ + RuleErrorBuilder::message(sprintf( + '@dataProvider %s related class not found.', + $dataProviderValue + )) + ->line($lineNumber) + ->identifier('phpunit.dataProviderClass') + ->build(), + ]; } try { $dataProviderMethodReflection = $classReflection->getNativeMethod($methodName); } catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) { - $error = RuleErrorBuilder::message(sprintf( - '@dataProvider %s related method not found.', - $dataProviderValue - ))->line($lineNumber)->build(); - - return [$error]; + return [ + RuleErrorBuilder::message(sprintf( + '@dataProvider %s related method not found.', + $dataProviderValue + )) + ->line($lineNumber) + ->identifier('phpunit.dataProviderMethod') + ->build(), + ]; } $errors = []; @@ -168,21 +174,30 @@ public function processDataProvider( '@dataProvider %s related method is used with incorrect case: %s.', $dataProviderValue, $dataProviderMethodReflection->getName() - ))->line($lineNumber)->build(); + )) + ->line($lineNumber) + ->identifier('method.nameCase') + ->build(); } if (!$dataProviderMethodReflection->isPublic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be public.', $dataProviderValue - ))->line($lineNumber)->build(); + )) + ->line($lineNumber) + ->identifier('phpunit.dataProviderPublic') + ->build(); } if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be static in PHPUnit 10 and newer.', $dataProviderValue - ))->line($lineNumber)->build(); + )) + ->line($lineNumber) + ->identifier('phpunit.dataProviderStatic') + ->build(); } return $errors; diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index 90dc0945..f93ef167 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -65,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array 'Trying to mock an undefined method %s() on class %s.', $method, implode('&', $mockClasses) - ))->build(); + ))->identifier('phpunit.mockMethod')->build(); continue; } @@ -83,7 +83,7 @@ public function processNode(Node $node, Scope $scope): array 'Trying to mock an undefined method %s() on class %s.', $method, implode('|', $classNames) - ))->build(); + ))->identifier('phpunit.mockMethod')->build(); } return $errors; diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index 5a640a1c..917d0bf9 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -57,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Missing call to parent::%s() method.', $methodName) - )->build(), + )->identifier('phpunit.callParent')->build(), ]; } From 4d99a7d020727a2c7b4b428aecbe6a0171bb78a5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 5 Jun 2023 10:03:19 +0200 Subject: [PATCH 206/277] Require PHPStan 1.11 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ea9f5cdf..dc0f3db2 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.11" }, "conflict": { "phpunit/phpunit": "<7.0" From 6536e6659038f49d37bf8c550098138edc38eccc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 25 Aug 2023 11:41:51 +0200 Subject: [PATCH 207/277] Open 1.3.x-dev --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index afe7e6ed..bc327e8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.1.x" + - "1.3.x" jobs: lint: From 62e883ea3762a31faa4f6609c20fb1363762d4e3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 25 Aug 2023 11:42:26 +0200 Subject: [PATCH 208/277] Open 1.4.x-dev --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bc327e8c..6e8c60a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.3.x" + - "1.4.x" jobs: lint: From 614acc10c522e319639bf38b0698a4a566665f04 Mon Sep 17 00:00:00 2001 From: Greg Korba Date: Fri, 25 Aug 2023 11:46:39 +0200 Subject: [PATCH 209/277] Add support for `assertObjectHasProperty` --- .../AssertTypeSpecifyingExtensionHelper.php | 3 +++ ...ssertFunctionTypeSpecifyingExtensionTest.php | 10 +++++++--- .../PHPUnit/data/assert-function-9.6.11.php | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 tests/Type/PHPUnit/data/assert-function-9.6.11.php diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 34d86ca1..963b4f57 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -276,6 +276,9 @@ private static function getExpressionResolvers(): array 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { return new FuncCall(new Name('property_exists'), [$object, $property]); }, + 'ObjectHasProperty' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { + return new FuncCall(new Name('property_exists'), [$object, $property]); + }, 'Contains' => static function (Scope $scope, Arg $needle, Arg $haystack): Expr { return new Expr\BinaryOp\BooleanOr( new Expr\Instanceof_($haystack->value, new Name('Traversable')), diff --git a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php index f2a254d7..2d43f8bc 100644 --- a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php @@ -11,11 +11,15 @@ class AssertFunctionTypeSpecifyingExtensionTest extends TypeInferenceTestCase /** @return mixed[] */ public function dataFileAsserts(): iterable { - if (!function_exists('PHPUnit\\Framework\\assertInstanceOf')) { - return []; + if (function_exists('PHPUnit\\Framework\\assertInstanceOf')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-function.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-function.php'); + if (function_exists('PHPUnit\\Framework\\assertObjectHasProperty')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-function-9.6.11.php'); + } + + return []; } /** diff --git a/tests/Type/PHPUnit/data/assert-function-9.6.11.php b/tests/Type/PHPUnit/data/assert-function-9.6.11.php new file mode 100644 index 00000000..dea6b851 --- /dev/null +++ b/tests/Type/PHPUnit/data/assert-function-9.6.11.php @@ -0,0 +1,17 @@ + Date: Mon, 11 Sep 2023 02:24:21 +0000 Subject: [PATCH 210/277] Update actions/checkout action to v4 --- .github/workflows/build.yml | 10 +++++----- .github/workflows/create-tag.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6e8c60a5..a93179c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -53,10 +53,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Checkout build-cs" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: "phpstan/build-cs" path: "build-cs" @@ -103,7 +103,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -146,7 +146,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 8452d986..a8535014 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -21,7 +21,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92b72547..e4a8ac62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Generate changelog id: changelog From 70ecacc64fe8090d8d2a33db5a51fe8e88acd93a Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 9 Oct 2023 20:57:48 +0200 Subject: [PATCH 211/277] AssertTypeSpecifyingExtensionHelper: rootExpr with unknown variable to avoid always-true false positives (#197) --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 13 ++++++++++++- tests/Rules/PHPUnit/data/assert-same.php | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 963b4f57..61bb6ddd 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -23,6 +23,7 @@ use ReflectionObject; use function array_key_exists; use function count; +use function in_array; use function strlen; use function strpos; use function substr; @@ -33,6 +34,12 @@ class AssertTypeSpecifyingExtensionHelper /** @var Closure[] */ private static $resolvers; + /** + * Those can specify types correctly, but would produce always-true issue + * @var string[] + */ + private static $resolversCausingAlwaysTrue = ['ContainsOnlyInstancesOf', 'ContainsEquals', 'Contains']; + /** * @param Arg[] $args */ @@ -87,10 +94,14 @@ public static function specifyTypes( if ($expression === null) { return new SpecifiedTypes([], []); } + + $bypassAlwaysTrueIssue = in_array(self::trimName($name), self::$resolversCausingAlwaysTrue, true); + return $typeSpecifier->specifyTypesInCondition( $scope, $expression, - TypeSpecifierContext::createTruthy() + TypeSpecifierContext::createTruthy(), + $bypassAlwaysTrueIssue ? new Expr\BinaryOp\BooleanAnd($expression, new Expr\Variable('nonsense')) : null ); } diff --git a/tests/Rules/PHPUnit/data/assert-same.php b/tests/Rules/PHPUnit/data/assert-same.php index 41384e5c..8ab297bd 100644 --- a/tests/Rules/PHPUnit/data/assert-same.php +++ b/tests/Rules/PHPUnit/data/assert-same.php @@ -65,6 +65,13 @@ public function testOther() $foo->assertSame(); } + public function testAssertContains() + { + $this->assertContains('not in the list', new \ArrayObject([1])); + $this->assertContainsEquals('not in the list', new \ArrayObject([1])); + $this->assertNotContains('not in the list', new \ArrayObject([1])); + } + public function testStaticMethodReturnWithSameTypeIsNotReported() { $this->assertSame(self::createSomething('foo'), self::createSomething('foo')); From 77345df14f04d05252c8b13a5b4444eacd6a0764 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 00:51:10 +0000 Subject: [PATCH 212/277] Update dessant/lock-threads action to v5 --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 4c7990df..c2b017b9 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -8,7 +8,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} issue-inactive-days: '31' From e95a58baebb1f2daf7067c3667bff371b202151d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 00:51:06 +0000 Subject: [PATCH 213/277] Update metcalfc/changelog-generator action to v4.2.0 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4a8ac62..2fb750a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.1.0 + uses: metcalfc/changelog-generator@v4.2.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 6a1df9a11edc5642f4a3b462fd1bcb7b07bafa35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 00:34:53 +0000 Subject: [PATCH 214/277] Update cbrgm/mastodon-github-action action to v2 --- .github/workflows/release-toot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml index 6a1c8156..1ba4fd77 100644 --- a/.github/workflows/release-toot.yml +++ b/.github/workflows/release-toot.yml @@ -10,7 +10,7 @@ jobs: toot: runs-on: ubuntu-latest steps: - - uses: cbrgm/mastodon-github-action@v1 + - uses: cbrgm/mastodon-github-action@v2 if: ${{ !github.event.repository.private }} with: # GitHub event payload From 77bdf9644e105833f810eefc3d6e051ac33dd939 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:51:55 +0000 Subject: [PATCH 215/277] Update metcalfc/changelog-generator action to v4.3.1 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2fb750a4..b1a669a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.2.0 + uses: metcalfc/changelog-generator@v4.3.1 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From d5242a59d035e46774f2e634b374bc39ff62cb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bundyra?= Date: Fri, 23 Feb 2024 09:51:20 +0000 Subject: [PATCH 216/277] Narrow type on assertCount and assertNotCount --- .../AssertTypeSpecifyingExtensionHelper.php | 14 +++++++++++++ tests/Type/PHPUnit/data/assert-function.php | 21 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 61bb6ddd..49512575 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -136,6 +136,20 @@ private static function getExpressionResolvers(): array { if (self::$resolvers === null) { self::$resolvers = [ + 'Count' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { + return new Identical( + $expected->value, + new FuncCall(new Name('count'), [$actual]) + ); + }, + 'NotCount' => static function (Scope $scope, Arg $expected, Arg $actual): BooleanNot { + return new BooleanNot( + new Identical( + $expected->value, + new FuncCall(new Name('count'), [$actual]) + ) + ); + }, 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): Instanceof_ { return new Instanceof_( $object->value, diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 15c4371a..10fb4176 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -7,6 +7,8 @@ use function PHPUnit\Framework\assertContains; use function PHPUnit\Framework\assertContainsEquals; use function PHPUnit\Framework\assertContainsOnlyInstancesOf; +use function PHPUnit\Framework\assertCount; +use function PHPUnit\Framework\assertNotCount; use function PHPUnit\Framework\assertEmpty; use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertObjectHasAttribute; @@ -90,4 +92,23 @@ public function containsOnlyInstancesOf(array $a, \Traversable $b): void assertType('Traversable', $b); } + public function count(array $a, \Countable $b): void + { + assertCount(3, $a); + assertType('non-empty-array', $a); + + assertCount(7, $b); + assertType('Countable', $b); + } + + public function notCount(array $a, array $b): void + { + assertNotCount(0, $a); + assertType('non-empty-array', $a); + + // still might be empty + assertNotCount(1, $b); + assertType('array', $b); + } + } From f3ea021866f4263f07ca3636bf22c64be9610c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Sat, 20 Apr 2024 08:39:00 +0200 Subject: [PATCH 217/277] Update lock-closed-issues.yml --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index c2b017b9..9a8fea7e 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -2,7 +2,7 @@ name: 'Lock Issues' on: schedule: - - cron: '0 0 * * *' + - cron: '7 0 * * *' jobs: lock: From bb6bec0008d6229e04632a46d4adb71aed63c269 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 4 Sep 2024 22:51:05 +0200 Subject: [PATCH 218/277] Pin build-cs --- .github/workflows/build.yml | 1 + Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a93179c2..63b2103d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,6 +60,7 @@ jobs: with: repository: "phpstan/build-cs" path: "build-cs" + ref: "1.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/Makefile b/Makefile index ecd8cfb2..b01b1537 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ lint: .PHONY: cs-install cs-install: git clone https://github.com/phpstan/build-cs.git || true - git -C build-cs fetch origin && git -C build-cs reset --hard origin/main + git -C build-cs fetch origin && git -C build-cs reset --hard origin/1.x composer install --working-dir build-cs .PHONY: cs From 146d9c32a7596ade35e9426ad748ee3b0e3c2566 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 4 Sep 2024 22:51:54 +0200 Subject: [PATCH 219/277] Test newer PHP versions --- .github/workflows/build.yml | 6 ++++++ composer.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63b2103d..270f0f51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" steps: - name: "Checkout" @@ -98,6 +100,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" @@ -141,6 +145,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" diff --git a/composer.json b/composer.json index dc0f3db2..5b1ec505 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^1.12" }, "conflict": { "phpunit/phpunit": "<7.0" From 7f1457f2fbea110c0a7be4a03747823f0575292f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 4 Sep 2024 22:52:21 +0200 Subject: [PATCH 220/277] Open 2.0.x --- .github/workflows/build.yml | 2 +- composer.json | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 270f0f51..0dc529dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.4.x" + - "2.0.x" jobs: lint: diff --git a/composer.json b/composer.json index 5b1ec505..5237f066 100644 --- a/composer.json +++ b/composer.json @@ -6,17 +6,16 @@ "MIT" ], "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" }, "conflict": { "phpunit/phpunit": "<7.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "config": { "platform": { From 953195d722a2c38c5ee904cea31d0a91b4d8a784 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 4 Sep 2024 22:56:08 +0200 Subject: [PATCH 221/277] Stop testing PHP 7.2 and 7.3 --- .github/workflows/build.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0dc529dd..8d659b18 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,8 +16,6 @@ jobs: strategy: matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -41,10 +39,6 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "Lint" run: "make lint" @@ -94,8 +88,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -124,10 +116,6 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "Tests" run: "make tests" @@ -139,8 +127,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -171,9 +157,5 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "PHPStan" run: "make phpstan" From 3faa60573a32522772e7cda004003b15466e2b5b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 4 Sep 2024 22:56:23 +0200 Subject: [PATCH 222/277] Update build-cs --- .github/workflows/build.yml | 2 +- Makefile | 2 +- .../MockObjectTypeNodeResolverExtension.php | 3 +- src/Rules/PHPUnit/AnnotationHelper.php | 2 +- src/Rules/PHPUnit/AssertRuleHelper.php | 2 +- src/Rules/PHPUnit/ClassCoversExistsRule.php | 12 +- .../PHPUnit/ClassMethodCoversExistsRule.php | 18 +- src/Rules/PHPUnit/CoversHelper.php | 13 +- .../PHPUnit/DataProviderDeclarationRule.php | 13 +- src/Rules/PHPUnit/DataProviderHelper.php | 23 +- .../PHPUnit/DataProviderHelperFactory.php | 6 +- src/Rules/PHPUnit/MockMethodCallRule.php | 8 +- .../NoMissingSpaceInClassAnnotationRule.php | 3 +- .../NoMissingSpaceInMethodAnnotationRule.php | 3 +- .../PHPUnit/ShouldCallParentMethodsRule.php | 2 +- .../AssertFunctionTypeSpecifyingExtension.php | 7 +- .../AssertMethodTypeSpecifyingExtension.php | 7 +- ...ertStaticMethodTypeSpecifyingExtension.php | 7 +- .../AssertTypeSpecifyingExtensionHelper.php | 238 +++++++----------- .../MockBuilderDynamicReturnTypeExtension.php | 2 +- .../MockObjectDynamicReturnTypeExtension.php | 4 +- .../PHPUnit/ClassCoversExistsRuleTest.php | 2 +- .../ClassMethodCoversExistsRuleTest.php | 2 +- 23 files changed, 153 insertions(+), 228 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d659b18..88543fb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: with: repository: "phpstan/build-cs" path: "build-cs" - ref: "1.x" + ref: "2.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/Makefile b/Makefile index b01b1537..1ee557df 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ lint: .PHONY: cs-install cs-install: git clone https://github.com/phpstan/build-cs.git || true - git -C build-cs fetch origin && git -C build-cs reset --hard origin/1.x + git -C build-cs fetch origin && git -C build-cs reset --hard origin/2.x composer install --working-dir build-cs .PHONY: cs diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php index 2d70b380..83f7b8b2 100644 --- a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -17,8 +17,7 @@ class MockObjectTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension { - /** @var TypeNodeResolver */ - private $typeNodeResolver; + private TypeNodeResolver $typeNodeResolver; public function setTypeNodeResolver(TypeNodeResolver $typeNodeResolver): void { diff --git a/src/Rules/PHPUnit/AnnotationHelper.php b/src/Rules/PHPUnit/AnnotationHelper.php index 4335bc81..21623cab 100644 --- a/src/Rules/PHPUnit/AnnotationHelper.php +++ b/src/Rules/PHPUnit/AnnotationHelper.php @@ -56,7 +56,7 @@ public function processDocComment(Doc $docComment): array } $errors[] = RuleErrorBuilder::message( - 'Annotation "' . $matches['annotation'] . '" is invalid, "@' . $matches['property'] . '" should be followed by a space and a value.' + 'Annotation "' . $matches['annotation'] . '" is invalid, "@' . $matches['property'] . '" should be followed by a space and a value.', )->identifier('phpunit.invalidPhpDoc')->build(); } diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index 3ad79c00..442b0655 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -30,7 +30,7 @@ public static function isMethodOrStaticCallOnAssert(Node $node, Scope $scope): b 'static', 'parent', ], - true + true, ) ) { $calledOnType = new ObjectType($scope->getClassReflection()->getName()); diff --git a/src/Rules/PHPUnit/ClassCoversExistsRule.php b/src/Rules/PHPUnit/ClassCoversExistsRule.php index bbced022..f8d831c9 100644 --- a/src/Rules/PHPUnit/ClassCoversExistsRule.php +++ b/src/Rules/PHPUnit/ClassCoversExistsRule.php @@ -23,16 +23,14 @@ class ClassCoversExistsRule implements Rule /** * Covers helper. * - * @var CoversHelper */ - private $coversHelper; + private CoversHelper $coversHelper; /** * Reflection provider. * - * @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; public function __construct( CoversHelper $coversHelper, @@ -62,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array if (count($classCoversDefaultClasses) >= 2) { return [ RuleErrorBuilder::message(sprintf( - '@coversDefaultClass is defined multiple times.' + '@coversDefaultClass is defined multiple times.', ))->identifier('phpunit.coversDuplicate')->build(), ]; } @@ -75,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array if (!$this->reflectionProvider->hasClass($className)) { $errors[] = RuleErrorBuilder::message(sprintf( '@coversDefaultClass references an invalid class %s.', - $className + $className, ))->identifier('phpunit.coversClass')->build(); } } @@ -83,7 +81,7 @@ public function processNode(Node $node, Scope $scope): array foreach ($classCovers as $covers) { $errors = array_merge( $errors, - $this->coversHelper->processCovers($node, $covers, null) + $this->coversHelper->processCovers($node, $covers, null), ); } diff --git a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php index 92cb1e2e..4bfdc417 100644 --- a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php +++ b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php @@ -25,16 +25,14 @@ class ClassMethodCoversExistsRule implements Rule /** * Covers helper. * - * @var CoversHelper */ - private $coversHelper; + private CoversHelper $coversHelper; /** * The file type mapper. * - * @var FileTypeMapper */ - private $fileTypeMapper; + private FileTypeMapper $fileTypeMapper; public function __construct( CoversHelper $coversHelper, @@ -65,9 +63,7 @@ public function processNode(Node $node, Scope $scope): array $classPhpDoc = $classReflection->getResolvedPhpDoc(); [$classCovers, $classCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($classPhpDoc); - $classCoversStrings = array_map(static function (PhpDocTagNode $covers): string { - return (string) $covers->value; - }, $classCovers); + $classCoversStrings = array_map(static fn (PhpDocTagNode $covers): string => (string) $covers->value, $classCovers); $docComment = $node->getDocComment(); if ($docComment === null) { @@ -83,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array $classReflection->getName(), $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $node->name->toString(), - $docComment->getText() + $docComment->getText(), ); [$methodCovers, $methodCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($methodPhpDoc); @@ -93,7 +89,7 @@ public function processNode(Node $node, Scope $scope): array if (count($methodCoversDefaultClasses) > 0) { $errors[] = RuleErrorBuilder::message(sprintf( '@coversDefaultClass defined on class method %s.', - $node->name + $node->name, ))->identifier('phpunit.covers')->build(); } @@ -101,13 +97,13 @@ public function processNode(Node $node, Scope $scope): array if (in_array((string) $covers->value, $classCoversStrings, true)) { $errors[] = RuleErrorBuilder::message(sprintf( 'Class already @covers %s so the method @covers is redundant.', - $covers->value + $covers->value, ))->identifier('phpunit.coversDuplicate')->build(); } $errors = array_merge( $errors, - $this->coversHelper->processCovers($node, $covers, $coversDefaultClass) + $this->coversHelper->processCovers($node, $covers, $coversDefaultClass), ); } diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index 55dbe8de..40ae561e 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -20,9 +20,8 @@ class CoversHelper /** * Reflection provider. * - * @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; public function __construct(ReflectionProvider $reflectionProvider) { @@ -48,12 +47,12 @@ public function getCoverAnnotations(?ResolvedPhpDocBlock $phpDoc): array foreach ($phpDocNodes as $docNode) { $covers = array_merge( $covers, - $docNode->getTagsByName('@covers') + $docNode->getTagsByName('@covers'), ); $coversDefaultClasses = array_merge( $coversDefaultClasses, - $docNode->getTagsByName('@coversDefaultClass') + $docNode->getTagsByName('@coversDefaultClass'), ); } @@ -100,14 +99,14 @@ public function processCovers( if ($class->isInterface()) { $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an interface.', - $fullName + $fullName, ))->identifier('phpunit.coversInterface')->build(); } if (isset($method) && $method !== '' && !$class->hasMethod($method)) { $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid method.', - $fullName + $fullName, ))->identifier('phpunit.coversMethod')->build(); } } elseif (isset($method) && $this->reflectionProvider->hasFunction(new Name($method, []), null)) { @@ -118,7 +117,7 @@ public function processCovers( $error = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid %s.', $fullName, - $isMethod ? 'method' : 'class or function' + $isMethod ? 'method' : 'class or function', ))->identifier(sprintf('phpunit.covers%s', $isMethod ? 'Method' : '')); if (strpos($className, '\\') === false) { diff --git a/src/Rules/PHPUnit/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php index 37c586de..cc44eeb4 100644 --- a/src/Rules/PHPUnit/DataProviderDeclarationRule.php +++ b/src/Rules/PHPUnit/DataProviderDeclarationRule.php @@ -17,23 +17,20 @@ class DataProviderDeclarationRule implements Rule /** * Data provider helper. * - * @var DataProviderHelper */ - private $dataProviderHelper; + private DataProviderHelper $dataProviderHelper; /** * When set to true, it reports data provider method with incorrect name case. * - * @var bool */ - private $checkFunctionNameCase; + private bool $checkFunctionNameCase; /** * When phpstan-deprecation-rules is installed, it reports deprecated usages. * - * @var bool */ - private $deprecationRulesInstalled; + private bool $deprecationRulesInstalled; public function __construct( DataProviderHelper $dataProviderHelper, @@ -70,8 +67,8 @@ public function processNode(Node $node, Scope $scope): array $dataProviderMethodName, $lineNumber, $this->checkFunctionNameCase, - $this->deprecationRulesInstalled - ) + $this->deprecationRulesInstalled, + ), ); } diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index 9d1b160a..ad354fa5 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -28,19 +28,16 @@ class DataProviderHelper /** * Reflection provider. * - * @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; /** * The file type mapper. * - * @var FileTypeMapper */ - private $fileTypeMapper; + private FileTypeMapper $fileTypeMapper; - /** @var bool */ - private $phpunit10OrNewer; + private bool $phpunit10OrNewer; public function __construct( ReflectionProvider $reflectionProvider, @@ -69,7 +66,7 @@ public function getDataProviderMethods( $classReflection->getName(), $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $node->name->toString(), - $docComment->getText() + $docComment->getText(), ); foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) { $dataProviderValue = $this->getDataProviderAnnotationValue($annotation); @@ -122,7 +119,7 @@ private function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array foreach ($phpDocNodes as $docNode) { $annotations = array_merge( $annotations, - $docNode->getTagsByName('@dataProvider') + $docNode->getTagsByName('@dataProvider'), ); } @@ -145,7 +142,7 @@ public function processDataProvider( return [ RuleErrorBuilder::message(sprintf( '@dataProvider %s related class not found.', - $dataProviderValue + $dataProviderValue, )) ->line($lineNumber) ->identifier('phpunit.dataProviderClass') @@ -159,7 +156,7 @@ public function processDataProvider( return [ RuleErrorBuilder::message(sprintf( '@dataProvider %s related method not found.', - $dataProviderValue + $dataProviderValue, )) ->line($lineNumber) ->identifier('phpunit.dataProviderMethod') @@ -173,7 +170,7 @@ public function processDataProvider( $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method is used with incorrect case: %s.', $dataProviderValue, - $dataProviderMethodReflection->getName() + $dataProviderMethodReflection->getName(), )) ->line($lineNumber) ->identifier('method.nameCase') @@ -183,7 +180,7 @@ public function processDataProvider( if (!$dataProviderMethodReflection->isPublic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be public.', - $dataProviderValue + $dataProviderValue, )) ->line($lineNumber) ->identifier('phpunit.dataProviderPublic') @@ -193,7 +190,7 @@ public function processDataProvider( if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be static in PHPUnit 10 and newer.', - $dataProviderValue + $dataProviderValue, )) ->line($lineNumber) ->identifier('phpunit.dataProviderStatic') diff --git a/src/Rules/PHPUnit/DataProviderHelperFactory.php b/src/Rules/PHPUnit/DataProviderHelperFactory.php index 7fc8af0f..9b965e2c 100644 --- a/src/Rules/PHPUnit/DataProviderHelperFactory.php +++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php @@ -14,11 +14,9 @@ class DataProviderHelperFactory { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var FileTypeMapper */ - private $fileTypeMapper; + private FileTypeMapper $fileTypeMapper; public function __construct(ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper) { diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index f93ef167..b6f8932a 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -54,9 +54,7 @@ public function processNode(Node $node, Scope $scope): array ) && !$type->hasMethod($method)->yes() ) { - $mockClasses = array_filter($type->getObjectClassNames(), static function (string $class): bool { - return $class !== MockObject::class && $class !== Stub::class; - }); + $mockClasses = array_filter($type->getObjectClassNames(), static fn (string $class): bool => $class !== MockObject::class && $class !== Stub::class); if (count($mockClasses) === 0) { continue; } @@ -64,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - implode('&', $mockClasses) + implode('&', $mockClasses), ))->identifier('phpunit.mockMethod')->build(); continue; } @@ -82,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - implode('|', $classNames) + implode('|', $classNames), ))->identifier('phpunit.mockMethod')->build(); } diff --git a/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php b/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php index 89e3e8ff..c8fd3a14 100644 --- a/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php +++ b/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php @@ -17,9 +17,8 @@ class NoMissingSpaceInClassAnnotationRule implements Rule /** * Covers helper. * - * @var AnnotationHelper */ - private $annotationHelper; + private AnnotationHelper $annotationHelper; public function __construct(AnnotationHelper $annotationHelper) { diff --git a/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php b/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php index 77577206..a44fc539 100644 --- a/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php +++ b/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php @@ -17,9 +17,8 @@ class NoMissingSpaceInMethodAnnotationRule implements Rule /** * Covers helper. * - * @var AnnotationHelper */ - private $annotationHelper; + private AnnotationHelper $annotationHelper; public function __construct(AnnotationHelper $annotationHelper) { diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index 917d0bf9..4035dcc9 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -56,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array if (!$hasParentCall) { return [ RuleErrorBuilder::message( - sprintf('Missing call to parent::%s() method.', $methodName) + sprintf('Missing call to parent::%s() method.', $methodName), )->identifier('phpunit.callParent')->build(), ]; } diff --git a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php index 31805a3f..f5a5a745 100644 --- a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php @@ -17,8 +17,7 @@ class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -33,7 +32,7 @@ public function isFunctionSupported( { return AssertTypeSpecifyingExtensionHelper::isSupported( $this->trimName($functionReflection->getName()), - $node->getArgs() + $node->getArgs(), ); } @@ -48,7 +47,7 @@ public function specifyTypes( $this->typeSpecifier, $scope, $this->trimName($functionReflection->getName()), - $node->getArgs() + $node->getArgs(), ); } diff --git a/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php index 6307f244..753c8b89 100644 --- a/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertMethodTypeSpecifyingExtension.php @@ -14,8 +14,7 @@ class AssertMethodTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -35,7 +34,7 @@ public function isMethodSupported( { return AssertTypeSpecifyingExtensionHelper::isSupported( $methodReflection->getName(), - $node->getArgs() + $node->getArgs(), ); } @@ -50,7 +49,7 @@ public function specifyTypes( $this->typeSpecifier, $scope, $functionReflection->getName(), - $node->getArgs() + $node->getArgs(), ); } diff --git a/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php index 54da4457..ec0dad14 100644 --- a/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertStaticMethodTypeSpecifyingExtension.php @@ -14,8 +14,7 @@ class AssertStaticMethodTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -35,7 +34,7 @@ public function isStaticMethodSupported( { return AssertTypeSpecifyingExtensionHelper::isSupported( $methodReflection->getName(), - $node->getArgs() + $node->getArgs(), ); } @@ -50,7 +49,7 @@ public function specifyTypes( $this->typeSpecifier, $scope, $functionReflection->getName(), - $node->getArgs() + $node->getArgs(), ); } diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 49512575..cdc6ff34 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -32,13 +32,13 @@ class AssertTypeSpecifyingExtensionHelper { /** @var Closure[] */ - private static $resolvers; + private static ?array $resolvers = null; /** * Those can specify types correctly, but would produce always-true issue * @var string[] */ - private static $resolversCausingAlwaysTrue = ['ContainsOnlyInstancesOf', 'ContainsEquals', 'Contains']; + private static array $resolversCausingAlwaysTrue = ['ContainsOnlyInstancesOf', 'ContainsEquals', 'Contains']; /** * @param Arg[] $args @@ -101,7 +101,7 @@ public static function specifyTypes( $scope, $expression, TypeSpecifierContext::createTruthy(), - $bypassAlwaysTrueIssue ? new Expr\BinaryOp\BooleanAnd($expression, new Expr\Variable('nonsense')) : null + $bypassAlwaysTrueIssue ? new Expr\BinaryOp\BooleanAnd($expression, new Expr\Variable('nonsense')) : null, ); } @@ -136,95 +136,57 @@ private static function getExpressionResolvers(): array { if (self::$resolvers === null) { self::$resolvers = [ - 'Count' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { - return new Identical( + 'Count' => static fn (Scope $scope, Arg $expected, Arg $actual): Identical => new Identical( + $expected->value, + new FuncCall(new Name('count'), [$actual]), + ), + 'NotCount' => static fn (Scope $scope, Arg $expected, Arg $actual): BooleanNot => new BooleanNot( + new Identical( $expected->value, - new FuncCall(new Name('count'), [$actual]) - ); - }, - 'NotCount' => static function (Scope $scope, Arg $expected, Arg $actual): BooleanNot { - return new BooleanNot( - new Identical( - $expected->value, - new FuncCall(new Name('count'), [$actual]) - ) - ); - }, - 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): Instanceof_ { - return new Instanceof_( - $object->value, - $class->value - ); - }, - 'Same' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { - return new Identical( - $expected->value, - $actual->value - ); - }, - 'True' => static function (Scope $scope, Arg $actual): Identical { - return new Identical( - $actual->value, - new ConstFetch(new Name('true')) - ); - }, - 'False' => static function (Scope $scope, Arg $actual): Identical { - return new Identical( - $actual->value, - new ConstFetch(new Name('false')) - ); - }, - 'Null' => static function (Scope $scope, Arg $actual): Identical { - return new Identical( - $actual->value, - new ConstFetch(new Name('null')) - ); - }, - 'Empty' => static function (Scope $scope, Arg $actual): Expr\BinaryOp\BooleanOr { - return new Expr\BinaryOp\BooleanOr( - new Instanceof_($actual->value, new Name(EmptyIterator::class)), - new Expr\BinaryOp\BooleanOr( - new Expr\BinaryOp\BooleanAnd( - new Instanceof_($actual->value, new Name(Countable::class)), - new Identical(new FuncCall(new Name('count'), [new Arg($actual->value)]), new LNumber(0)) - ), - new Expr\Empty_($actual->value) - ) - ); - }, - 'IsArray' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_array'), [$actual]); - }, - 'IsBool' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_bool'), [$actual]); - }, - 'IsCallable' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_callable'), [$actual]); - }, - 'IsFloat' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_float'), [$actual]); - }, - 'IsInt' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_int'), [$actual]); - }, - 'IsIterable' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_iterable'), [$actual]); - }, - 'IsNumeric' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_numeric'), [$actual]); - }, - 'IsObject' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_object'), [$actual]); - }, - 'IsResource' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_resource'), [$actual]); - }, - 'IsString' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_string'), [$actual]); - }, - 'IsScalar' => static function (Scope $scope, Arg $actual): FuncCall { - return new FuncCall(new Name('is_scalar'), [$actual]); - }, + new FuncCall(new Name('count'), [$actual]), + ), + ), + 'InstanceOf' => static fn (Scope $scope, Arg $class, Arg $object): Instanceof_ => new Instanceof_( + $object->value, + $class->value, + ), + 'Same' => static fn (Scope $scope, Arg $expected, Arg $actual): Identical => new Identical( + $expected->value, + $actual->value, + ), + 'True' => static fn (Scope $scope, Arg $actual): Identical => new Identical( + $actual->value, + new ConstFetch(new Name('true')), + ), + 'False' => static fn (Scope $scope, Arg $actual): Identical => new Identical( + $actual->value, + new ConstFetch(new Name('false')), + ), + 'Null' => static fn (Scope $scope, Arg $actual): Identical => new Identical( + $actual->value, + new ConstFetch(new Name('null')), + ), + 'Empty' => static fn (Scope $scope, Arg $actual): Expr\BinaryOp\BooleanOr => new Expr\BinaryOp\BooleanOr( + new Instanceof_($actual->value, new Name(EmptyIterator::class)), + new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + new Instanceof_($actual->value, new Name(Countable::class)), + new Identical(new FuncCall(new Name('count'), [new Arg($actual->value)]), new LNumber(0)), + ), + new Expr\Empty_($actual->value), + ), + ), + 'IsArray' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_array'), [$actual]), + 'IsBool' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_bool'), [$actual]), + 'IsCallable' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_callable'), [$actual]), + 'IsFloat' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_float'), [$actual]), + 'IsInt' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_int'), [$actual]), + 'IsIterable' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_iterable'), [$actual]), + 'IsNumeric' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_numeric'), [$actual]), + 'IsObject' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_object'), [$actual]), + 'IsResource' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_resource'), [$actual]), + 'IsString' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_string'), [$actual]), + 'IsScalar' => static fn (Scope $scope, Arg $actual): FuncCall => new FuncCall(new Name('is_scalar'), [$actual]), 'InternalType' => static function (Scope $scope, Arg $type, Arg $value): ?FuncCall { $typeNames = $scope->getType($type->value)->getConstantStrings(); if (count($typeNames) !== 1) { @@ -286,61 +248,49 @@ private static function getExpressionResolvers(): array new Name($functionName), [ $value, - ] - ); - }, - 'ArrayHasKey' => static function (Scope $scope, Arg $key, Arg $array): Expr { - return new Expr\BinaryOp\BooleanOr( - new Expr\BinaryOp\BooleanAnd( - new Expr\Instanceof_($array->value, new Name('ArrayAccess')), - new Expr\MethodCall($array->value, 'offsetExists', [$key]) - ), - new FuncCall(new Name('array_key_exists'), [$key, $array]) - ); - }, - 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { - return new FuncCall(new Name('property_exists'), [$object, $property]); - }, - 'ObjectHasProperty' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { - return new FuncCall(new Name('property_exists'), [$object, $property]); - }, - 'Contains' => static function (Scope $scope, Arg $needle, Arg $haystack): Expr { - return new Expr\BinaryOp\BooleanOr( - new Expr\Instanceof_($haystack->value, new Name('Traversable')), - new FuncCall(new Name('in_array'), [$needle, $haystack, new Arg(new ConstFetch(new Name('true')))]) - ); - }, - 'ContainsEquals' => static function (Scope $scope, Arg $needle, Arg $haystack): Expr { - return new Expr\BinaryOp\BooleanOr( - new Expr\Instanceof_($haystack->value, new Name('Traversable')), - new Expr\BinaryOp\BooleanAnd( - new Expr\BooleanNot(new Expr\Empty_($haystack->value)), - new FuncCall(new Name('in_array'), [$needle, $haystack, new Arg(new ConstFetch(new Name('false')))]) - ) - ); - }, - 'ContainsOnlyInstancesOf' => static function (Scope $scope, Arg $className, Arg $haystack): Expr { - return new Expr\BinaryOp\BooleanOr( - new Expr\Instanceof_($haystack->value, new Name('Traversable')), - new Identical( - $haystack->value, - new FuncCall(new Name('array_filter'), [ - $haystack, - new Arg(new Expr\Closure([ - 'static' => true, - 'params' => [ - new Param(new Expr\Variable('_')), - ], - 'stmts' => [ - new Stmt\Return_( - new FuncCall(new Name('is_a'), [new Arg(new Expr\Variable('_')), $className]) - ), - ], - ])), - ]) - ) + ], ); }, + 'ArrayHasKey' => static fn (Scope $scope, Arg $key, Arg $array): Expr => new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + new Expr\Instanceof_($array->value, new Name('ArrayAccess')), + new Expr\MethodCall($array->value, 'offsetExists', [$key]), + ), + new FuncCall(new Name('array_key_exists'), [$key, $array]), + ), + 'ObjectHasAttribute' => static fn (Scope $scope, Arg $property, Arg $object): FuncCall => new FuncCall(new Name('property_exists'), [$object, $property]), + 'ObjectHasProperty' => static fn (Scope $scope, Arg $property, Arg $object): FuncCall => new FuncCall(new Name('property_exists'), [$object, $property]), + 'Contains' => static fn (Scope $scope, Arg $needle, Arg $haystack): Expr => new Expr\BinaryOp\BooleanOr( + new Expr\Instanceof_($haystack->value, new Name('Traversable')), + new FuncCall(new Name('in_array'), [$needle, $haystack, new Arg(new ConstFetch(new Name('true')))]), + ), + 'ContainsEquals' => static fn (Scope $scope, Arg $needle, Arg $haystack): Expr => new Expr\BinaryOp\BooleanOr( + new Expr\Instanceof_($haystack->value, new Name('Traversable')), + new Expr\BinaryOp\BooleanAnd( + new Expr\BooleanNot(new Expr\Empty_($haystack->value)), + new FuncCall(new Name('in_array'), [$needle, $haystack, new Arg(new ConstFetch(new Name('false')))]), + ), + ), + 'ContainsOnlyInstancesOf' => static fn (Scope $scope, Arg $className, Arg $haystack): Expr => new Expr\BinaryOp\BooleanOr( + new Expr\Instanceof_($haystack->value, new Name('Traversable')), + new Identical( + $haystack->value, + new FuncCall(new Name('array_filter'), [ + $haystack, + new Arg(new Expr\Closure([ + 'static' => true, + 'params' => [ + new Param(new Expr\Variable('_')), + ], + 'stmts' => [ + new Stmt\Return_( + new FuncCall(new Name('is_a'), [new Arg(new Expr\Variable('_')), $className]), + ), + ], + ])), + ]), + ), + ), ]; } diff --git a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php index 5390b11c..166a9038 100644 --- a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php @@ -27,7 +27,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool 'getMockForAbstractClass', 'getMockForTrait', ], - true + true, ); } diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php index 4f74fe68..626f168c 100644 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -31,9 +31,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { $type = $scope->getType($methodCall->var); - $mockClasses = array_values(array_filter($type->getObjectClassNames(), static function (string $class): bool { - return $class !== MockObject::class; - })); + $mockClasses = array_values(array_filter($type->getObjectClassNames(), static fn (string $class): bool => $class !== MockObject::class)); if (count($mockClasses) !== 1) { return new ObjectType(InvocationMocker::class); diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php index 69a7de2f..2a835eaf 100644 --- a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule return new ClassCoversExistsRule( new CoversHelper($reflection), - $reflection + $reflection, ); } diff --git a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php index b886b460..45e8b1f0 100644 --- a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php @@ -18,7 +18,7 @@ protected function getRule(): Rule return new ClassMethodCoversExistsRule( new CoversHelper($reflection), - self::getContainer()->getByType(FileTypeMapper::class) + self::getContainer()->getByType(FileTypeMapper::class), ); } From 4d861e0843cd1f8eccacfac14e24a8629280a887 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 13 Sep 2024 14:47:01 +0200 Subject: [PATCH 223/277] Fix after TypeSpecifier BC break --- .../PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index cdc6ff34..04def4e3 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -101,8 +101,7 @@ public static function specifyTypes( $scope, $expression, TypeSpecifierContext::createTruthy(), - $bypassAlwaysTrueIssue ? new Expr\BinaryOp\BooleanAnd($expression, new Expr\Variable('nonsense')) : null, - ); + )->setRootExpr($bypassAlwaysTrueIssue ? new Expr\BinaryOp\BooleanAnd($expression, new Expr\Variable('nonsense')) : $expression); } /** From 09e2d3b470bdda02824c626735153dfd962e3f29 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Sep 2024 18:07:03 +0200 Subject: [PATCH 224/277] Uncover everything behind the bleedingEdge flag --- rules.neon | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/rules.neon b/rules.neon index 8dc7056b..023e11c1 100644 --- a/rules.neon +++ b/rules.neon @@ -2,28 +2,18 @@ rules: - PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule - PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule - PHPStan\Rules\PHPUnit\AssertSameWithCountRule + - PHPStan\Rules\PHPUnit\ClassCoversExistsRule + - PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule - PHPStan\Rules\PHPUnit\MockMethodCallRule + - PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule + - PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule - PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule services: - - class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule - - class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule - class: PHPStan\Rules\PHPUnit\DataProviderDeclarationRule arguments: checkFunctionNameCase: %checkFunctionNameCase% deprecationRulesInstalled: %deprecationRulesInstalled% - - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule - - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule - -conditionalTags: - PHPStan\Rules\PHPUnit\ClassCoversExistsRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% - PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% - PHPStan\Rules\PHPUnit\DataProviderDeclarationRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% - PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% - PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% + tags: + - phpstan.rules.rule From 3cc855474263ad6220dfa49167cbea34ca1dd300 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 05:16:27 +0200 Subject: [PATCH 225/277] Fixes after PHPStan update --- tests/Type/PHPUnit/data/assert-function.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 10fb4176..851e07b0 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -38,7 +38,7 @@ public function assertInstanceOfWorksWithTemplate($o, $class): void public function arrayHasNumericKey(array $a, \ArrayAccess $b): void { assertArrayHasKey(0, $a); - assertType('array&hasOffset(0)', $a); + assertType('non-empty-array&hasOffset(0)', $a); assertArrayHasKey(0, $b); assertType('ArrayAccess', $b); @@ -47,7 +47,7 @@ public function arrayHasNumericKey(array $a, \ArrayAccess $b): void { public function arrayHasStringKey(array $a, \ArrayAccess $b): void { assertArrayHasKey('key', $a); - assertType("array&hasOffset('key')", $a); + assertType("non-empty-array&hasOffset('key')", $a); assertArrayHasKey('key', $b); assertType("ArrayAccess", $b); From 11d4235fbc6313ecbf93708606edfd3222e44949 Mon Sep 17 00:00:00 2001 From: Michael Telgmann Date: Tue, 12 Nov 2024 11:21:05 +0100 Subject: [PATCH 226/277] fix: Make sure Assert::assertIsList correctly asserts an array as list --- extension.neon | 3 +++ stubs/Assert.stub | 13 +++++++++++++ stubs/AssertionFailedError.stub | 8 ++++++++ stubs/ExpectationFailedException.stub | 8 ++++++++ 4 files changed, 32 insertions(+) create mode 100644 stubs/Assert.stub create mode 100644 stubs/AssertionFailedError.stub create mode 100644 stubs/ExpectationFailedException.stub diff --git a/extension.neon b/extension.neon index cea2b155..8de21f54 100644 --- a/extension.neon +++ b/extension.neon @@ -9,6 +9,9 @@ parameters: - markTestIncomplete - markTestSkipped stubFiles: + - stubs/Assert.stub + - stubs/AssertionFailedError.stub + - stubs/ExpectationFailedException.stub - stubs/InvocationMocker.stub - stubs/MockBuilder.stub - stubs/MockObject.stub diff --git a/stubs/Assert.stub b/stubs/Assert.stub new file mode 100644 index 00000000..01df95a1 --- /dev/null +++ b/stubs/Assert.stub @@ -0,0 +1,13 @@ + Date: Tue, 12 Nov 2024 13:48:00 +0100 Subject: [PATCH 227/277] Fix --- stubs/Assert.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/Assert.stub b/stubs/Assert.stub index 01df95a1..d9ccd12b 100644 --- a/stubs/Assert.stub +++ b/stubs/Assert.stub @@ -5,7 +5,7 @@ namespace PHPUnit\Framework; abstract class Assert { /** - * @phpstan-assert list $array + * @phpstan-assert list $array * * @throws ExpectationFailedException */ From 72a6721c9b64b3e4c9db55abbc38f790b318267e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 13 Dec 2024 11:48:43 +0100 Subject: [PATCH 228/277] Use more specific node-type --- src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 6 +++--- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 6 +++--- src/Rules/PHPUnit/AssertSameWithCountRule.php | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 969f7b3c..308f5147 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -3,22 +3,22 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\ConstFetch; -use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function count; /** - * @implements Rule + * @implements Rule */ class AssertSameBooleanExpectedRule implements Rule { public function getNodeType(): string { - return NodeAbstract::class; + return CallLike::class; } public function processNode(Node $node, Scope $scope): array diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index a2d0cfa0..363fa578 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -3,22 +3,22 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\ConstFetch; -use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function count; /** - * @implements Rule + * @implements Rule */ class AssertSameNullExpectedRule implements Rule { public function getNodeType(): string { - return NodeAbstract::class; + return CallLike::class; } public function processNode(Node $node, Scope $scope): array diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 38fdf9ab..3c3ada4b 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -4,7 +4,7 @@ use Countable; use PhpParser\Node; -use PhpParser\NodeAbstract; +use PhpParser\Node\Expr\CallLike; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -12,14 +12,14 @@ use function count; /** - * @implements Rule + * @implements Rule */ class AssertSameWithCountRule implements Rule { public function getNodeType(): string { - return NodeAbstract::class; + return CallLike::class; } public function processNode(Node $node, Scope $scope): array From 10880dad5190e788634a62ebb211802669382f3d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 17 Dec 2024 18:22:01 +0100 Subject: [PATCH 229/277] Implement AssertEqualsIsDiscouragedRule (#216) --- README.md | 1 + composer.json | 2 +- rules.neon | 7 ++ .../PHPUnit/AssertEqualsIsDiscouragedRule.php | 64 +++++++++++++++++++ .../AssertEqualsIsDiscouragedRuleTest.php | 38 +++++++++++ .../data/assert-equals-is-discouraged.php | 37 +++++++++++ 6 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php create mode 100644 tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php diff --git a/README.md b/README.md index 205cbe4b..526085b9 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ It also contains this strict framework-specific rules (can be enabled separately * Check that you are not using `assertSame()` with `false` as expected value. `assertFalse()` should be used instead. * Check that you are not using `assertSame()` with `null` as expected value. `assertNull()` should be used instead. * Check that you are not using `assertSame()` with `count($variable)` as second parameter. `assertCount($variable)` should be used instead. +* Check that you are not using `assertEquals()` with same types (`assertSame()` should be used) ## How to document mock objects in phpDocs? diff --git a/composer.json b/composer.json index 5237f066..3453041e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.0.4" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/rules.neon b/rules.neon index 023e11c1..84b71498 100644 --- a/rules.neon +++ b/rules.neon @@ -9,6 +9,10 @@ rules: - PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule - PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule +conditionalTags: + PHPStan\Rules\PHPUnit\AssertEqualsIsDiscouragedRule: + phpstan.rules.rule: [%strictRulesInstalled%, %featureToggles.bleedingEdge%] + services: - class: PHPStan\Rules\PHPUnit\DataProviderDeclarationRule @@ -17,3 +21,6 @@ services: deprecationRulesInstalled: %deprecationRulesInstalled% tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\PHPUnit\AssertEqualsIsDiscouragedRule diff --git a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php new file mode 100644 index 00000000..7aea39a5 --- /dev/null +++ b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php @@ -0,0 +1,64 @@ + + */ +class AssertEqualsIsDiscouragedRule implements Rule +{ + + public function getNodeType(): string + { + return CallLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { + return []; + } + + if (count($node->getArgs()) < 2) { + return []; + } + if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertequals') { + return []; + } + + $leftType = TypeCombinator::removeNull($scope->getType($node->getArgs()[0]->value)); + $rightType = TypeCombinator::removeNull($scope->getType($node->getArgs()[1]->value)); + + if ($leftType->isConstantScalarValue()->yes()) { + $leftType = $leftType->generalize(GeneralizePrecision::lessSpecific()); + } + if ($rightType->isConstantScalarValue()->yes()) { + $rightType = $rightType->generalize(GeneralizePrecision::lessSpecific()); + } + + if ( + ($leftType->isScalar()->yes() && $rightType->isScalar()->yes()) + && ($leftType->isSuperTypeOf($rightType)->yes()) + && ($rightType->isSuperTypeOf($leftType)->yes()) + ) { + return [ + RuleErrorBuilder::message( + 'You should use assertSame() instead of assertEquals(), because both values are scalars of the same type', + )->identifier('phpunit.assertEquals')->build(), + ]; + } + + return []; + } + +} diff --git a/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php new file mode 100644 index 00000000..e739ee49 --- /dev/null +++ b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php @@ -0,0 +1,38 @@ + + */ +final class AssertEqualsIsDiscouragedRuleTest extends RuleTestCase +{ + + private const ERROR_MESSAGE = 'You should use assertSame() instead of assertEquals(), because both values are scalars of the same type'; + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/assert-equals-is-discouraged.php'], [ + [self::ERROR_MESSAGE, 19], + [self::ERROR_MESSAGE, 22], + [self::ERROR_MESSAGE, 23], + [self::ERROR_MESSAGE, 24], + [self::ERROR_MESSAGE, 25], + [self::ERROR_MESSAGE, 26], + [self::ERROR_MESSAGE, 27], + [self::ERROR_MESSAGE, 28], + [self::ERROR_MESSAGE, 29], + [self::ERROR_MESSAGE, 30], + [self::ERROR_MESSAGE, 32], + ]); + } + + protected function getRule(): Rule + { + return new AssertEqualsIsDiscouragedRule(); + } + +} diff --git a/tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php b/tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php new file mode 100644 index 00000000..727408d1 --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php @@ -0,0 +1,37 @@ +assertSame(5, $integer); + static::assertSame(5, $integer); + + $this->assertEquals('', $string); + $this->assertEquals(null, $string); + static::assertEquals(null, $string); + static::assertEquals($nullableString, $string); + $this->assertEquals(2, $integer); + $this->assertEquals(2.2, $float); + static::assertEquals((int) '2', (int) $string); + $this->assertEquals(true, $boolean); + $this->assertEquals($string, $string); + $this->assertEquals($integer, $integer); + $this->assertEquals($boolean, $boolean); + $this->assertEquals($float, $float); + $this->assertEquals($null, $null); + $this->assertEquals((string) new Exception(), (string) new Exception()); + $this->assertEquals([], []); + $this->assertEquals(new Exception(), new Exception()); + static::assertEquals(new Exception(), new Exception()); + } +} From e32ac656788a5bf3dedda89e6a2cad5643bf1a18 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Dec 2024 09:27:11 +0100 Subject: [PATCH 230/277] Support assertNotEquals in AssertEqualsIsDiscouragedRule --- src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php | 6 +++++- tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php | 3 +++ tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php index 7aea39a5..f4fc89c0 100644 --- a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php +++ b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php @@ -10,6 +10,7 @@ use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\TypeCombinator; use function count; +use function in_array; use function strtolower; /** @@ -32,7 +33,10 @@ public function processNode(Node $node, Scope $scope): array if (count($node->getArgs()) < 2) { return []; } - if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertequals') { + if ( + !$node->name instanceof Node\Identifier + || !in_array(strtolower($node->name->name), ['assertequals', 'assertnotequals'], true) + ) { return []; } diff --git a/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php index e739ee49..f1d34d0e 100644 --- a/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php @@ -27,6 +27,9 @@ public function testRule(): void [self::ERROR_MESSAGE, 29], [self::ERROR_MESSAGE, 30], [self::ERROR_MESSAGE, 32], + [self::ERROR_MESSAGE, 37], + [self::ERROR_MESSAGE, 38], + [self::ERROR_MESSAGE, 39], ]); } diff --git a/tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php b/tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php index 727408d1..7f4d80e1 100644 --- a/tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php +++ b/tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php @@ -33,5 +33,11 @@ public function dummyTest(string $string, int $integer, bool $boolean, float $fl $this->assertEquals([], []); $this->assertEquals(new Exception(), new Exception()); static::assertEquals(new Exception(), new Exception()); + + $this->assertNotEquals($string, $string); + $this->assertNotEquals($integer, $integer); + $this->assertNotEquals($boolean, $boolean); + $this->assertNotSame(5, $integer); + static::assertNotSame(5, $integer); } } From d09e152f403c843998d7a52b5d87040c937525dd Mon Sep 17 00:00:00 2001 From: PrinsFrank <25006490+PrinsFrank@users.noreply.github.com> Date: Wed, 22 Jan 2025 14:07:38 +0100 Subject: [PATCH 231/277] Fix error message for "assertNotEquals" usage referencing "assertSame" and "assertEquals" --- README.md | 1 + .../PHPUnit/AssertEqualsIsDiscouragedRule.php | 7 ++++- .../AssertEqualsIsDiscouragedRuleTest.php | 31 ++++++++++--------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 526085b9..34693790 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ It also contains this strict framework-specific rules (can be enabled separately * Check that you are not using `assertSame()` with `null` as expected value. `assertNull()` should be used instead. * Check that you are not using `assertSame()` with `count($variable)` as second parameter. `assertCount($variable)` should be used instead. * Check that you are not using `assertEquals()` with same types (`assertSame()` should be used) +* Check that you are not using `assertNotEquals()` with same types (`assertNotSame()` should be used) ## How to document mock objects in phpDocs? diff --git a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php index f4fc89c0..bd685dd7 100644 --- a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php +++ b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php @@ -11,6 +11,7 @@ use PHPStan\Type\TypeCombinator; use function count; use function in_array; +use function sprintf; use function strtolower; /** @@ -57,7 +58,11 @@ public function processNode(Node $node, Scope $scope): array ) { return [ RuleErrorBuilder::message( - 'You should use assertSame() instead of assertEquals(), because both values are scalars of the same type', + sprintf( + 'You should use %s() instead of %s(), because both values are scalars of the same type', + strtolower($node->name->name) === 'assertnotequals' ? 'assertNotSame' : 'assertSame', + $node->name->name, + ), )->identifier('phpunit.assertEquals')->build(), ]; } diff --git a/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php index f1d34d0e..568b5b6f 100644 --- a/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php @@ -11,25 +11,26 @@ final class AssertEqualsIsDiscouragedRuleTest extends RuleTestCase { - private const ERROR_MESSAGE = 'You should use assertSame() instead of assertEquals(), because both values are scalars of the same type'; + private const ERROR_MESSAGE_EQUALS = 'You should use assertSame() instead of assertEquals(), because both values are scalars of the same type'; + private const ERROR_MESSAGE_NOT_EQUALS = 'You should use assertNotSame() instead of assertNotEquals(), because both values are scalars of the same type'; public function testRule(): void { $this->analyse([__DIR__ . '/data/assert-equals-is-discouraged.php'], [ - [self::ERROR_MESSAGE, 19], - [self::ERROR_MESSAGE, 22], - [self::ERROR_MESSAGE, 23], - [self::ERROR_MESSAGE, 24], - [self::ERROR_MESSAGE, 25], - [self::ERROR_MESSAGE, 26], - [self::ERROR_MESSAGE, 27], - [self::ERROR_MESSAGE, 28], - [self::ERROR_MESSAGE, 29], - [self::ERROR_MESSAGE, 30], - [self::ERROR_MESSAGE, 32], - [self::ERROR_MESSAGE, 37], - [self::ERROR_MESSAGE, 38], - [self::ERROR_MESSAGE, 39], + [self::ERROR_MESSAGE_EQUALS, 19], + [self::ERROR_MESSAGE_EQUALS, 22], + [self::ERROR_MESSAGE_EQUALS, 23], + [self::ERROR_MESSAGE_EQUALS, 24], + [self::ERROR_MESSAGE_EQUALS, 25], + [self::ERROR_MESSAGE_EQUALS, 26], + [self::ERROR_MESSAGE_EQUALS, 27], + [self::ERROR_MESSAGE_EQUALS, 28], + [self::ERROR_MESSAGE_EQUALS, 29], + [self::ERROR_MESSAGE_EQUALS, 30], + [self::ERROR_MESSAGE_EQUALS, 32], + [self::ERROR_MESSAGE_NOT_EQUALS, 37], + [self::ERROR_MESSAGE_NOT_EQUALS, 38], + [self::ERROR_MESSAGE_NOT_EQUALS, 39], ]); } From 17bbfd3532b399d57ba9f9d788711e79c487b593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 28 Jan 2025 10:24:59 +0100 Subject: [PATCH 232/277] Update LICENSE --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index d0053746..52fba1e2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2016 Ondřej Mirtes +Copyright (c) 2025 PHPStan s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 0f857bfcea0d73d7dc4b506c40c8c2a5fffc3191 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 18 Mar 2025 10:49:50 +0100 Subject: [PATCH 233/277] Introduce phpstan-deprecation-rules --- composer.json | 1 + phpstan.neon | 1 + src/Rules/PHPUnit/ClassCoversExistsRule.php | 2 +- src/Rules/PHPUnit/ClassMethodCoversExistsRule.php | 2 +- src/Rules/PHPUnit/DataProviderDeclarationRule.php | 2 +- src/Rules/PHPUnit/DataProviderHelper.php | 6 +++--- src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php | 2 +- src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php | 2 +- src/Rules/PHPUnit/ShouldCallParentMethodsRule.php | 2 +- 9 files changed, 11 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 3453041e..212c518f 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.6" }, diff --git a/phpstan.neon b/phpstan.neon index 2b8fa1ae..7c96296a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,7 @@ includes: - extension.neon - rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon - phar://phpstan.phar/conf/bleedingEdge.neon - phpstan-baseline.neon diff --git a/src/Rules/PHPUnit/ClassCoversExistsRule.php b/src/Rules/PHPUnit/ClassCoversExistsRule.php index f8d831c9..a36317ef 100644 --- a/src/Rules/PHPUnit/ClassCoversExistsRule.php +++ b/src/Rules/PHPUnit/ClassCoversExistsRule.php @@ -50,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array { $classReflection = $node->getClassReflection(); - if (!$classReflection->isSubclassOf(TestCase::class)) { + if (!$classReflection->is(TestCase::class)) { return []; } diff --git a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php index 4bfdc417..dd328f83 100644 --- a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php +++ b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php @@ -56,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$classReflection->isSubclassOf(TestCase::class)) { + if (!$classReflection->is(TestCase::class)) { return []; } diff --git a/src/Rules/PHPUnit/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php index cc44eeb4..1983493c 100644 --- a/src/Rules/PHPUnit/DataProviderDeclarationRule.php +++ b/src/Rules/PHPUnit/DataProviderDeclarationRule.php @@ -52,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array { $classReflection = $scope->getClassReflection(); - if ($classReflection === null || !$classReflection->isSubclassOf(TestCase::class)) { + if ($classReflection === null || !$classReflection->is(TestCase::class)) { return []; } diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index ad354fa5..aad678e3 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -76,7 +76,7 @@ public function getDataProviderMethods( } $dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue); - $dataProviderMethod[] = $node->getLine(); + $dataProviderMethod[] = $node->getStartLine(); yield $dataProviderValue => $dataProviderMethod; } @@ -257,7 +257,7 @@ private function parseDataProviderExternalAttribute(Attribute $attribute): ?arra sprintf('%s::%s', $className, $methodNameArg->value) => [ $dataProviderClassReflection, $methodNameArg->value, - $attribute->getLine(), + $attribute->getStartLine(), ], ]; } @@ -279,7 +279,7 @@ private function parseDataProviderAttribute(Attribute $attribute, ClassReflectio $methodNameArg->value => [ $classReflection, $methodNameArg->value, - $attribute->getLine(), + $attribute->getStartLine(), ], ]; } diff --git a/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php b/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php index c8fd3a14..a2fc39f1 100644 --- a/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php +++ b/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php @@ -33,7 +33,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $classReflection = $scope->getClassReflection(); - if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) { + if ($classReflection === null || $classReflection->is(TestCase::class) === false) { return []; } diff --git a/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php b/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php index a44fc539..906e60b1 100644 --- a/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php +++ b/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php @@ -33,7 +33,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $classReflection = $scope->getClassReflection(); - if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) { + if ($classReflection === null || $classReflection->is(TestCase::class) === false) { return []; } diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index 4035dcc9..bfd31690 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -33,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$scope->getClassReflection()->isSubclassOf(TestCase::class)) { + if (!$scope->getClassReflection()->is(TestCase::class)) { return []; } From 855b82c4e70c0274ca099585d1dcf026a5485a2d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 23 Mar 2025 14:57:01 +0100 Subject: [PATCH 234/277] Remove config.platform --- composer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/composer.json b/composer.json index 212c518f..60ba1e7a 100644 --- a/composer.json +++ b/composer.json @@ -19,9 +19,6 @@ "phpunit/phpunit": "^9.6" }, "config": { - "platform": { - "php": "7.4.6" - }, "sort-packages": true }, "extra": { From 846d1612c1f1f589d3ffdbf683d5bc82b60ffd0c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 23 Mar 2025 14:58:54 +0100 Subject: [PATCH 235/277] Test multiple PHPUnit versions --- .github/workflows/build.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88543fb5..07de1083 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,6 +97,21 @@ jobs: dependencies: - "lowest" - "highest" + phpunit-version: + - "^9.5" + - "^10.5" + - "^11.5" + exclude: + - php-version: "7.4" + phpunit-version: "^10.5" + - php-version: "8.0" + phpunit-version: "^10.5" + - php-version: "7.4" + phpunit-version: "^11.5" + - php-version: "8.0" + phpunit-version: "^11.5" + - php-version: "8.1" + phpunit-version: "^11.5" steps: - name: "Checkout" @@ -108,6 +123,9 @@ jobs: coverage: "none" php-version: "${{ matrix.php-version }}" + - name: "Require specific PHPUnit version" + run: "composer require --dev phpunit/phpunit:${{ matrix.phpunit-version }}" + - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} run: "composer update --prefer-lowest --no-interaction --no-progress" @@ -136,6 +154,21 @@ jobs: dependencies: - "lowest" - "highest" + phpunit-version: + - "^9.5" + - "^10.5" + - "^11.5" + exclude: + - php-version: "7.4" + phpunit-version: "^10.5" + - php-version: "8.0" + phpunit-version: "^10.5" + - php-version: "7.4" + phpunit-version: "^11.5" + - php-version: "8.0" + phpunit-version: "^11.5" + - php-version: "8.1" + phpunit-version: "^11.5" steps: - name: "Checkout" @@ -149,6 +182,9 @@ jobs: extensions: mbstring tools: composer:v2 + - name: "Require specific PHPUnit version" + run: "composer require --dev phpunit/phpunit:${{ matrix.phpunit-version }}" + - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} run: "composer update --prefer-lowest --no-interaction --no-progress" From 342b6c18973fd1205b67ad59a83a316cfb2ee45e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 23 Mar 2025 15:05:58 +0100 Subject: [PATCH 236/277] Data providers must be static --- .../PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php | 6 +++--- .../PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php index 2d43f8bc..4405c4d6 100644 --- a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php @@ -9,14 +9,14 @@ class AssertFunctionTypeSpecifyingExtensionTest extends TypeInferenceTestCase { /** @return mixed[] */ - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { if (function_exists('PHPUnit\\Framework\\assertInstanceOf')) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-function.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/assert-function.php'); } if (function_exists('PHPUnit\\Framework\\assertObjectHasProperty')) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-function-9.6.11.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/assert-function-9.6.11.php'); } return []; diff --git a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php index e1841e0b..0b36c2b9 100644 --- a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php @@ -8,9 +8,9 @@ class AssertMethodTypeSpecifyingExtensionTest extends TypeInferenceTestCase { /** @return mixed[] */ - public function dataFileAsserts(): iterable + public static function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-method.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/assert-method.php'); } /** From 630aa9981ec9ca809ab2e2f6e69f18bc790ae6aa Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 23 Mar 2025 15:09:31 +0100 Subject: [PATCH 237/277] Remove deprecated assert --- tests/Type/PHPUnit/data/assert-function.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 851e07b0..0fabf59e 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -53,12 +53,6 @@ public function arrayHasStringKey(array $a, \ArrayAccess $b): void assertType("ArrayAccess", $b); } - public function objectHasAttribute(object $a): void - { - assertObjectHasAttribute('property', $a); - assertType("object&hasProperty(property)", $a); - } - public function testEmpty($a): void { assertEmpty($a); From 19f8059bdc64a6234c45483f46a3e034c7dd06c2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 23 Mar 2025 15:15:12 +0100 Subject: [PATCH 238/277] Do not generate code coverage --- phpunit.xml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 8f71615a..420ef740 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -12,20 +12,6 @@ failOnWarning="true" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xml" > - - - ./src - - - - - - - tests From eb88670836752d7f4a231512f4da407f24cf458a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 23 Mar 2025 15:23:33 +0100 Subject: [PATCH 239/277] Always install nikic/php-parser v5 --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 60ba1e7a..be6b3bd7 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "phpunit/phpunit": "<7.0" }, "require-dev": { + "nikic/php-parser": "^5", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", From 1a07095989e956832e1fd0f36eb422c1ab8ca725 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 23 Mar 2025 23:01:32 +0100 Subject: [PATCH 240/277] Add non regression test for #222 --- phpstan-baseline.neon | 5 +++ tests/Rules/Methods/CallMethodsRuleTest.php | 34 +++++++++++++++++++++ tests/Rules/Methods/data/bug-222.php | 34 +++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 tests/Rules/Methods/CallMethodsRuleTest.php create mode 100644 tests/Rules/Methods/data/bug-222.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 53fb96b8..f56b3cfb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -14,3 +14,8 @@ parameters: message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" count: 1 path: tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php + + - + message: "#^Accessing PHPStan\\\\Rules\\\\Methods\\\\CallMethodsRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/Rules/Methods/CallMethodsRuleTest.php diff --git a/tests/Rules/Methods/CallMethodsRuleTest.php b/tests/Rules/Methods/CallMethodsRuleTest.php new file mode 100644 index 00000000..99d572f1 --- /dev/null +++ b/tests/Rules/Methods/CallMethodsRuleTest.php @@ -0,0 +1,34 @@ + + */ +class CallMethodsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(CallMethodsRule::class); + } + + public function testBug222(): void + { + $this->analyse([__DIR__ . '/data/bug-222.php'], []); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/Methods/data/bug-222.php b/tests/Rules/Methods/data/bug-222.php new file mode 100644 index 00000000..d07ca146 --- /dev/null +++ b/tests/Rules/Methods/data/bug-222.php @@ -0,0 +1,34 @@ +expects($this->exactly(1)) + ->method('get') + ->with(24) + ->willReturn('24'); + + $mockService + ->method('get') + ->with(24) + ->willReturn('24'); + + $mockService + ->expects($this->exactly(1)) + ->method('get') + ->willReturn('24'); + + $mockService + ->method('get') + ->willReturn('24'); + } + +} From 40fbbc1e6fe722f9c244b90b14c097c39bc77989 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 02:28:06 +0000 Subject: [PATCH 241/277] chore(deps): update metcalfc/changelog-generator action to v4.5.0 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1a669a9..be6cad08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.3.1 + uses: metcalfc/changelog-generator@v4.5.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From bf031aeef3eae57052c8275553d4e688d07a3b99 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 23 Mar 2025 15:26:26 +0100 Subject: [PATCH 242/277] Test PHPUnit v12 --- .github/workflows/build.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07de1083..65f351a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -101,6 +101,7 @@ jobs: - "^9.5" - "^10.5" - "^11.5" + - "^12.0.9" exclude: - php-version: "7.4" phpunit-version: "^10.5" @@ -112,6 +113,14 @@ jobs: phpunit-version: "^11.5" - php-version: "8.1" phpunit-version: "^11.5" + - php-version: "7.4" + phpunit-version: "^12.0.9" + - php-version: "8.0" + phpunit-version: "^12.0.9" + - php-version: "8.1" + phpunit-version: "^12.0.9" + - php-version: "8.2" + phpunit-version: "^12.0.9" steps: - name: "Checkout" @@ -158,6 +167,7 @@ jobs: - "^9.5" - "^10.5" - "^11.5" + - "^12.0.9" exclude: - php-version: "7.4" phpunit-version: "^10.5" @@ -169,6 +179,14 @@ jobs: phpunit-version: "^11.5" - php-version: "8.1" phpunit-version: "^11.5" + - php-version: "7.4" + phpunit-version: "^12.0.9" + - php-version: "8.0" + phpunit-version: "^12.0.9" + - php-version: "8.1" + phpunit-version: "^12.0.9" + - php-version: "8.2" + phpunit-version: "^12.0.9" steps: - name: "Checkout" From 1f36fc548cf0af5db4ca4d12891f82d4cc7bcc96 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Mar 2025 14:53:55 +0100 Subject: [PATCH 243/277] Use DataProvider attribute --- phpstan.neon | 5 +++++ .../PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php | 2 ++ .../Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php | 2 ++ 3 files changed, 9 insertions(+) diff --git a/phpstan.neon b/phpstan.neon index 7c96296a..57379453 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,3 +9,8 @@ includes: parameters: excludePaths: - tests/*/data/* + ignoreErrors: + - + message: '#^Attribute class PHPUnit\\Framework\\Attributes\\DataProvider does not exist\.$#' + identifier: attribute.notFound + reportUnmatched: false diff --git a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php index 4405c4d6..40140174 100644 --- a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\PHPUnit; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use function function_exists; class AssertFunctionTypeSpecifyingExtensionTest extends TypeInferenceTestCase @@ -26,6 +27,7 @@ public static function dataFileAsserts(): iterable * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, diff --git a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php index 0b36c2b9..8c6ebb8b 100644 --- a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\PHPUnit; use PHPStan\Testing\TypeInferenceTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class AssertMethodTypeSpecifyingExtensionTest extends TypeInferenceTestCase { @@ -17,6 +18,7 @@ public static function dataFileAsserts(): iterable * @dataProvider dataFileAsserts * @param mixed ...$args */ + #[DataProvider('dataFileAsserts')] public function testFileAsserts( string $assertType, string $file, From 4d2b44bdba358f0a1990247f8b59f6c31185e600 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Mar 2025 15:10:24 +0100 Subject: [PATCH 244/277] InvocationMocker class no longer exists --- extension.neon | 9 --- src/Rules/PHPUnit/MockMethodCallRule.php | 65 ++++++++++++------- ...cationMockerDynamicReturnTypeExtension.php | 30 --------- .../MockObjectDynamicReturnTypeExtension.php | 43 ------------ stubs/InvocationMocker.stub | 13 ---- .../Rules/PHPUnit/MockMethodCallRuleTest.php | 10 +-- 6 files changed, 43 insertions(+), 127 deletions(-) delete mode 100644 src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php delete mode 100644 src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php delete mode 100644 stubs/InvocationMocker.stub diff --git a/extension.neon b/extension.neon index 8de21f54..117c9922 100644 --- a/extension.neon +++ b/extension.neon @@ -12,7 +12,6 @@ parameters: - stubs/Assert.stub - stubs/AssertionFailedError.stub - stubs/ExpectationFailedException.stub - - stubs/InvocationMocker.stub - stubs/MockBuilder.stub - stubs/MockObject.stub - stubs/Stub.stub @@ -42,18 +41,10 @@ services: class: PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension tags: - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension - - - class: PHPStan\Type\PHPUnit\InvocationMockerDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - class: PHPStan\Type\PHPUnit\MockBuilderDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - - class: PHPStan\Type\PHPUnit\MockObjectDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension - class: PHPStan\Rules\PHPUnit\CoversHelper - diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index b6f8932a..e953d18e 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -5,9 +5,10 @@ use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPStan\Type\Type; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\Stub; use function array_filter; @@ -47,44 +48,58 @@ public function processNode(Node $node, Scope $scope): array $method = $constantString->getValue(); $type = $scope->getType($node->var); - if ( - ( - in_array(MockObject::class, $type->getObjectClassNames(), true) - || in_array(Stub::class, $type->getObjectClassNames(), true) - ) - && !$type->hasMethod($method)->yes() - ) { - $mockClasses = array_filter($type->getObjectClassNames(), static fn (string $class): bool => $class !== MockObject::class && $class !== Stub::class); - if (count($mockClasses) === 0) { - continue; - } - - $errors[] = RuleErrorBuilder::message(sprintf( - 'Trying to mock an undefined method %s() on class %s.', - $method, - implode('&', $mockClasses), - ))->identifier('phpunit.mockMethod')->build(); + $error = $this->checkCallOnType($type, $method); + if ($error !== null) { + $errors[] = $error; continue; } - $mockedClassObject = $type->getTemplateType(InvocationMocker::class, 'TMockedClass'); - if ($mockedClassObject->hasMethod($method)->yes()) { + if (!$node->var instanceof MethodCall) { continue; } - $classNames = $mockedClassObject->getObjectClassNames(); - if (count($classNames) === 0) { + if (!$node->var->name instanceof Node\Identifier) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf( + if ($node->var->name->toLowerString() !== 'expects') { + continue; + } + + $varType = $scope->getType($node->var->var); + $error = $this->checkCallOnType($varType, $method); + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + + private function checkCallOnType(Type $type, string $method): ?IdentifierRuleError + { + if ( + ( + in_array(MockObject::class, $type->getObjectClassNames(), true) + || in_array(Stub::class, $type->getObjectClassNames(), true) + ) + && !$type->hasMethod($method)->yes() + ) { + $mockClasses = array_filter($type->getObjectClassNames(), static fn (string $class): bool => $class !== MockObject::class && $class !== Stub::class); + if (count($mockClasses) === 0) { + return null; + } + + return RuleErrorBuilder::message(sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - implode('|', $classNames), + implode('&', $mockClasses), ))->identifier('phpunit.mockMethod')->build(); } - return $errors; + return null; } } diff --git a/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php b/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php deleted file mode 100644 index 44764f62..00000000 --- a/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php +++ /dev/null @@ -1,30 +0,0 @@ -getName() !== 'getMatcher'; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - return $scope->getType($methodCall->var); - } - -} diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php deleted file mode 100644 index 626f168c..00000000 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ /dev/null @@ -1,43 +0,0 @@ -getName() === 'expects'; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - $type = $scope->getType($methodCall->var); - $mockClasses = array_values(array_filter($type->getObjectClassNames(), static fn (string $class): bool => $class !== MockObject::class)); - - if (count($mockClasses) !== 1) { - return new ObjectType(InvocationMocker::class); - } - - return new GenericObjectType(InvocationMocker::class, [new ObjectType($mockClasses[0])]); - } - -} diff --git a/stubs/InvocationMocker.stub b/stubs/InvocationMocker.stub deleted file mode 100644 index c58719f5..00000000 --- a/stubs/InvocationMocker.stub +++ /dev/null @@ -1,13 +0,0 @@ - @@ -28,14 +27,11 @@ public function testRule(): void 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', 20, ], - ]; - - if (interface_exists('PHPUnit\Framework\MockObject\Builder\InvocationStubber')) { - $expectedErrors[] = [ + [ 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', 36, - ]; - } + ], + ]; $this->analyse([__DIR__ . '/data/mock-method-call.php'], $expectedErrors); } From 0aef32ff3a4e5f01b94d2373aae586aea0af6ed8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Mar 2025 13:43:47 +0100 Subject: [PATCH 245/277] Improve logic MockMethodCallRule to search for method even on wrong type --- src/Rules/PHPUnit/MockMethodCallRule.php | 18 ++++---- .../Rules/PHPUnit/MockMethodCallRuleTest.php | 5 +++ tests/Rules/PHPUnit/data/bug-227.php | 42 +++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 tests/Rules/PHPUnit/data/bug-227.php diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index e953d18e..6c3b0dc4 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -48,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array $method = $constantString->getValue(); $type = $scope->getType($node->var); - $error = $this->checkCallOnType($type, $method); + $error = $this->checkCallOnType($scope, $type, $method); if ($error !== null) { $errors[] = $error; continue; @@ -67,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array } $varType = $scope->getType($node->var->var); - $error = $this->checkCallOnType($varType, $method); + $error = $this->checkCallOnType($scope, $varType, $method); if ($error === null) { continue; } @@ -78,14 +78,16 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - private function checkCallOnType(Type $type, string $method): ?IdentifierRuleError + private function checkCallOnType(Scope $scope, Type $type, string $method): ?IdentifierRuleError { + $methodReflection = $scope->getMethodReflection($type, $method); + if ($methodReflection !== null) { + return null; + } + if ( - ( - in_array(MockObject::class, $type->getObjectClassNames(), true) - || in_array(Stub::class, $type->getObjectClassNames(), true) - ) - && !$type->hasMethod($method)->yes() + in_array(MockObject::class, $type->getObjectClassNames(), true) + || in_array(Stub::class, $type->getObjectClassNames(), true) ) { $mockClasses = array_filter($type->getObjectClassNames(), static fn (string $class): bool => $class !== MockObject::class && $class !== Stub::class); if (count($mockClasses) === 0) { diff --git a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php index a7f73085..284b2820 100644 --- a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php @@ -36,6 +36,11 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/mock-method-call.php'], $expectedErrors); } + public function testBug227(): void + { + $this->analyse([__DIR__ . '/data/bug-227.php'], []); + } + /** * @return string[] */ diff --git a/tests/Rules/PHPUnit/data/bug-227.php b/tests/Rules/PHPUnit/data/bug-227.php new file mode 100644 index 00000000..18a4d4a6 --- /dev/null +++ b/tests/Rules/PHPUnit/data/bug-227.php @@ -0,0 +1,42 @@ +tsfe = $this->getMockBuilder(Foo::class) + ->onlyMethods(['addCacheTags', 'getLanguage']) + ->disableOriginalConstructor() + ->getMock(); + $this->tsfe->method('getLanguage')->willReturn('aaa'); + } + + public function testSometest(): void + { + $this->tsfe->expects(self::once())->method('addCacheTags'); + } +} From 6b92469f8a7995e626da3aa487099617b8dfa260 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Mar 2025 13:47:06 +0100 Subject: [PATCH 246/277] Fix build --- tests/Rules/PHPUnit/MockMethodCallRuleTest.php | 4 ++++ tests/Rules/PHPUnit/data/bug-227.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php index 284b2820..f7e89c7a 100644 --- a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -38,6 +39,9 @@ public function testRule(): void public function testBug227(): void { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } $this->analyse([__DIR__ . '/data/bug-227.php'], []); } diff --git a/tests/Rules/PHPUnit/data/bug-227.php b/tests/Rules/PHPUnit/data/bug-227.php index 18a4d4a6..1efe6c1c 100644 --- a/tests/Rules/PHPUnit/data/bug-227.php +++ b/tests/Rules/PHPUnit/data/bug-227.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug227; From d35895e0388e9aa822af00e1dfd4f8086fd8938d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 03:35:39 +0000 Subject: [PATCH 247/277] chore(deps): update metcalfc/changelog-generator action to v4.6.2 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be6cad08..b8c96d48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.5.0 + uses: metcalfc/changelog-generator@v4.6.2 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 22c6949f5c0d4d3d343fbd42d2bd75472089d135 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Jun 2025 10:34:24 +0200 Subject: [PATCH 248/277] Make `phpunit.dataProviderStatic` auto-fixable --- composer.json | 2 +- extension.neon | 2 ++ src/Rules/PHPUnit/DataProviderHelper.php | 28 +++++++++++++++++-- .../PHPUnit/DataProviderHelperFactory.php | 12 ++++++-- .../DataProviderDeclarationRuleTest.php | 7 ++++- .../PHPUnit/data/data-provider-static-fix.php | 21 ++++++++++++++ .../data/data-provider-static-fix.php.fixed | 21 ++++++++++++++ 7 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 tests/Rules/PHPUnit/data/data-provider-static-fix.php create mode 100644 tests/Rules/PHPUnit/data/data-provider-static-fix.php.fixed diff --git a/composer.json b/composer.json index be6b3bd7..3c07436d 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.0.4" + "phpstan/phpstan": "^2.1.18" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/extension.neon b/extension.neon index 117c9922..28900524 100644 --- a/extension.neon +++ b/extension.neon @@ -54,6 +54,8 @@ services: factory: @PHPStan\Rules\PHPUnit\DataProviderHelperFactory::create() - class: PHPStan\Rules\PHPUnit\DataProviderHelperFactory + arguments: + parser: @defaultAnalysisParser conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index aad678e3..338d7167 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -2,12 +2,15 @@ namespace PHPStan\Rules\PHPUnit; +use PhpParser\Modifiers; use PhpParser\Node\Attribute; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Name; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\NodeFinder; use PHPStan\Analyser\Scope; +use PHPStan\Parser\Parser; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\Reflection\ClassReflection; @@ -37,16 +40,20 @@ class DataProviderHelper */ private FileTypeMapper $fileTypeMapper; + private Parser $parser; + private bool $phpunit10OrNewer; public function __construct( ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper, + Parser $parser, bool $phpunit10OrNewer ) { $this->reflectionProvider = $reflectionProvider; $this->fileTypeMapper = $fileTypeMapper; + $this->parser = $parser; $this->phpunit10OrNewer = $phpunit10OrNewer; } @@ -188,13 +195,28 @@ public function processDataProvider( } if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) { - $errors[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be static in PHPUnit 10 and newer.', $dataProviderValue, )) ->line($lineNumber) - ->identifier('phpunit.dataProviderStatic') - ->build(); + ->identifier('phpunit.dataProviderStatic'); + + $dataProviderMethodReflectionDeclaringClass = $dataProviderMethodReflection->getDeclaringClass(); + if ($dataProviderMethodReflectionDeclaringClass->getFileName() !== null) { + $stmts = $this->parser->parseFile($dataProviderMethodReflectionDeclaringClass->getFileName()); + $nodeFinder = new NodeFinder(); + /** @var ClassMethod|null $methodNode */ + $methodNode = $nodeFinder->findFirst($stmts, static fn ($node) => $node instanceof ClassMethod && $node->name->toString() === $dataProviderMethodReflection->getName()); + if ($methodNode !== null) { + $errorBuilder->fixNode($methodNode, static function (ClassMethod $methodNode) { + $methodNode->flags |= Modifiers::STATIC; + + return $methodNode; + }); + } + } + $errors[] = $errorBuilder->build(); } return $errors; diff --git a/src/Rules/PHPUnit/DataProviderHelperFactory.php b/src/Rules/PHPUnit/DataProviderHelperFactory.php index 9b965e2c..a0768c8a 100644 --- a/src/Rules/PHPUnit/DataProviderHelperFactory.php +++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\PHPUnit; +use PHPStan\Parser\Parser; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\FileTypeMapper; use PHPUnit\Framework\TestCase; @@ -18,10 +19,17 @@ class DataProviderHelperFactory private FileTypeMapper $fileTypeMapper; - public function __construct(ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper) + private Parser $parser; + + public function __construct( + ReflectionProvider $reflectionProvider, + FileTypeMapper $fileTypeMapper, + Parser $parser + ) { $this->reflectionProvider = $reflectionProvider; $this->fileTypeMapper = $fileTypeMapper; + $this->parser = $parser; } public function create(): DataProviderHelper @@ -49,7 +57,7 @@ public function create(): DataProviderHelper } } - return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $phpUnit10OrNewer); + return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $this->parser, $phpUnit10OrNewer); } } diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index 18cc12b3..cbe40bf1 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule $reflection = $this->createReflectionProvider(); return new DataProviderDeclarationRule( - new DataProviderHelper($reflection, self::getContainer()->getByType(FileTypeMapper::class),true), + new DataProviderHelper($reflection, self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getService('defaultAnalysisParser'), true), true, true ); @@ -65,6 +65,11 @@ public function testRule(): void ]); } + public function testFixDataProviderStatic(): void + { + $this->fix(__DIR__ . '/data/data-provider-static-fix.php', __DIR__ . '/data/data-provider-static-fix.php.fixed'); + } + /** * @return string[] */ diff --git a/tests/Rules/PHPUnit/data/data-provider-static-fix.php b/tests/Rules/PHPUnit/data/data-provider-static-fix.php new file mode 100644 index 00000000..1f8005a2 --- /dev/null +++ b/tests/Rules/PHPUnit/data/data-provider-static-fix.php @@ -0,0 +1,21 @@ + Date: Sun, 13 Jul 2025 13:29:09 +0200 Subject: [PATCH 249/277] Check isFirstClassCallable before accessing getArgs --- src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php | 4 ++++ src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 4 ++++ src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 4 ++++ src/Rules/PHPUnit/AssertSameWithCountRule.php | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php index bd685dd7..7257eec5 100644 --- a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php +++ b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php @@ -31,6 +31,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->isFirstClassCallable()) { + return []; + } + if (count($node->getArgs()) < 2) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 308f5147..9abbd75a 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -27,6 +27,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->isFirstClassCallable()) { + return []; + } + if (count($node->getArgs()) < 2) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 363fa578..83807ec9 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -27,6 +27,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->isFirstClassCallable()) { + return []; + } + if (count($node->getArgs()) < 2) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 3c3ada4b..9e051cc8 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -28,6 +28,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->isFirstClassCallable()) { + return []; + } + if (count($node->getArgs()) < 2) { return []; } From 9a9b161baee88a5f5c58d816943cff354ff233dc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Jul 2025 12:42:18 +0200 Subject: [PATCH 250/277] Make AssertEqualsIsDiscouragedRule auto-fixable --- .../PHPUnit/AssertEqualsIsDiscouragedRule.php | 17 +++++++++++-- .../AssertEqualsIsDiscouragedRuleTest.php | 5 ++++ .../assert-equals-is-discouraged-fixable.php | 24 +++++++++++++++++++ ...rt-equals-is-discouraged-fixable.php.fixed | 24 +++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 tests/Rules/PHPUnit/data/assert-equals-is-discouraged-fixable.php create mode 100644 tests/Rules/PHPUnit/data/assert-equals-is-discouraged-fixable.php.fixed diff --git a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php index 7257eec5..b03f0830 100644 --- a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php +++ b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php @@ -60,14 +60,27 @@ public function processNode(Node $node, Scope $scope): array && ($leftType->isSuperTypeOf($rightType)->yes()) && ($rightType->isSuperTypeOf($leftType)->yes()) ) { + $correctName = strtolower($node->name->name) === 'assertnotequals' ? 'assertNotSame' : 'assertSame'; return [ RuleErrorBuilder::message( sprintf( 'You should use %s() instead of %s(), because both values are scalars of the same type', - strtolower($node->name->name) === 'assertnotequals' ? 'assertNotSame' : 'assertSame', + $correctName, $node->name->name, ), - )->identifier('phpunit.assertEquals')->build(), + )->identifier('phpunit.assertEquals') + ->fixNode($node, static function (CallLike $node) use ($correctName) { + if ($node instanceof Node\Expr\MethodCall) { + $node->name = new Node\Identifier($correctName); + } + + if ($node instanceof Node\Expr\StaticCall) { + $node->name = new Node\Identifier($correctName); + } + + return $node; + }) + ->build(), ]; } diff --git a/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php index 568b5b6f..040c1eee 100644 --- a/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php @@ -34,6 +34,11 @@ public function testRule(): void ]); } + public function testFix(): void + { + $this->fix(__DIR__ . '/data/assert-equals-is-discouraged-fixable.php', __DIR__ . '/data/assert-equals-is-discouraged-fixable.php.fixed'); + } + protected function getRule(): Rule { return new AssertEqualsIsDiscouragedRule(); diff --git a/tests/Rules/PHPUnit/data/assert-equals-is-discouraged-fixable.php b/tests/Rules/PHPUnit/data/assert-equals-is-discouraged-fixable.php new file mode 100644 index 00000000..5c0c993b --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-equals-is-discouraged-fixable.php @@ -0,0 +1,24 @@ +assertEquals('', $s); + $this->assertNotEquals('', $t); + } + + public function doFoo2(string $s, string $t): void + { + self::assertEquals('', $s); + self::assertNotEquals('', $t); + } + +} diff --git a/tests/Rules/PHPUnit/data/assert-equals-is-discouraged-fixable.php.fixed b/tests/Rules/PHPUnit/data/assert-equals-is-discouraged-fixable.php.fixed new file mode 100644 index 00000000..9217e3e1 --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-equals-is-discouraged-fixable.php.fixed @@ -0,0 +1,24 @@ +assertSame('', $s); + $this->assertNotSame('', $t); + } + + public function doFoo2(string $s, string $t): void + { + self::assertSame('', $s); + self::assertNotSame('', $t); + } + +} From 1d7eb7e5d8e7daefb033df1beaa497dfeed72c04 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:00:11 +0000 Subject: [PATCH 251/277] chore(deps): update eomm/why-don-t-you-tweet action to v2 --- .github/workflows/release-tweet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-tweet.yml b/.github/workflows/release-tweet.yml index 09b39ded..d81f34ca 100644 --- a/.github/workflows/release-tweet.yml +++ b/.github/workflows/release-tweet.yml @@ -10,7 +10,7 @@ jobs: tweet: runs-on: ubuntu-latest steps: - - uses: Eomm/why-don-t-you-tweet@v1 + - uses: Eomm/why-don-t-you-tweet@v2 if: ${{ !github.event.repository.private }} with: # GitHub event payload From b1df1f52d17f04ad6670fd2b6ffbda99d0b63bb6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 02:01:02 +0000 Subject: [PATCH 252/277] chore(deps): update actions/checkout action to v5 --- .github/workflows/build.yml | 10 +++++----- .github/workflows/create-tag.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 65f351a3..28ab39c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -49,10 +49,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Checkout build-cs" - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: "phpstan/build-cs" path: "build-cs" @@ -124,7 +124,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -190,7 +190,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index a8535014..fd918164 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -21,7 +21,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8c96d48..ed7e51ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Generate changelog id: changelog From 9ec3fa951a29922c8f7d456fc04a7ccc9bb2ae81 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Tue, 2 Sep 2025 18:14:57 +0200 Subject: [PATCH 253/277] Modernize code examples in README.md --- README.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 34693790..c86df268 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This extension provides following features: * `createMock()`, `getMockForAbstractClass()` and `getMockFromWsdl()` methods return an intersection type (see the [detailed explanation of intersection types](https://phpstan.org/blog/union-types-vs-intersection-types)) of the mock object and the mocked class so that both methods from the mock object (like `expects`) and from the mocked class are available on the object. * `getMock()` called on `MockBuilder` is also supported. -* Interprets `Foo|PHPUnit_Framework_MockObject_MockObject` in phpDoc so that it results in an intersection type instead of a union type. +* Interprets `Foo|MockObject` in phpDoc so that it results in an intersection type instead of a union type. * Defines early terminating method calls for the `PHPUnit\Framework\TestCase` class to prevent undefined variable errors. * Specifies types of expressions passed to various `assert` methods like `assertInstanceOf`, `assertTrue`, `assertInternalType` etc. * Combined with PHPStan's level 4, it points out always-true and always-false asserts like `assertTrue(true)` etc. @@ -27,18 +27,15 @@ It also contains this strict framework-specific rules (can be enabled separately ## How to document mock objects in phpDocs? -If you need to configure the mock even after you assign it to a property or return it from a method, you should add `PHPUnit_Framework_MockObject_MockObject` to the phpDoc: +If you need to configure the mock even after you assign it to a property or return it from a method, you should add `\PHPUnit\Framework\MockObject\MockObject` to the type: ```php -/** - * @return Foo&PHPUnit_Framework_MockObject_MockObject - */ -private function createFooMock() +private function createFooMock(): Foo&\PHPUnit\Framework\MockObject\MockObject { return $this->createMock(Foo::class); } -public function testSomething() +public function testSomething(): void { $fooMock = $this->createFooMock(); $fooMock->method('doFoo')->will($this->returnValue('test')); @@ -46,22 +43,33 @@ public function testSomething() } ``` -Please note that the correct syntax for intersection types is `Foo&PHPUnit_Framework_MockObject_MockObject`. `Foo|PHPUnit_Framework_MockObject_MockObject` is also supported, but only for ecosystem and legacy reasons. +If you cannot use native intersection types yet, you can use PHPDoc instead. + +```php +/** + * @return Foo&\PHPUnit\Framework\MockObject\MockObject + */ +private function createFooMock(): Foo +{ + return $this->createMock(Foo::class); +} +``` + +Please note that the correct syntax for intersection types is `Foo&\PHPUnit\Framework\MockObject\MockObject`. `Foo|\PHPUnit\Framework\MockObject\MockObject` is also supported, but only for ecosystem and legacy reasons. If the mock is fully configured and only the methods of the mocked class are supposed to be called on the value, it's fine to typehint only the mocked class: ```php -/** @var Foo */ -private $foo; +private Foo $foo; -protected function setUp() +protected function setUp(): void { $fooMock = $this->createMock(Foo::class); $fooMock->method('doFoo')->will($this->returnValue('test')); $this->foo = $fooMock; } -public function testSomething() +public function testSomething(): void { $this->foo->doFoo(); // $this->foo->method() and expects() can no longer be called From c54a26903d4a34b6d2a13b004f505ba340aabcf4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 24 Sep 2025 13:42:52 +0200 Subject: [PATCH 254/277] Narrow too wide return type --- src/Rules/PHPUnit/DataProviderHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index 338d7167..5f65656f 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -285,7 +285,7 @@ private function parseDataProviderExternalAttribute(Attribute $attribute): ?arra } /** - * @return array|null + * @return array|null */ private function parseDataProviderAttribute(Attribute $attribute, ClassReflection $classReflection): ?array { From 39950c70b6a4faa36567b83b8c4b32f889c68e03 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 14 Oct 2025 12:27:41 +0200 Subject: [PATCH 255/277] Preparational refactoring for DataProviderDataRule --- extension.neon | 3 + phpstan.neon | 2 + src/Rules/PHPUnit/DataProviderHelper.php | 141 ++++++++++++------ .../PHPUnit/DataProviderHelperFactory.php | 37 +---- src/Rules/PHPUnit/PHPUnitVersionDetector.php | 57 +++++++ 5 files changed, 163 insertions(+), 77 deletions(-) create mode 100644 src/Rules/PHPUnit/PHPUnitVersionDetector.php diff --git a/extension.neon b/extension.neon index 28900524..70831227 100644 --- a/extension.neon +++ b/extension.neon @@ -49,6 +49,9 @@ services: class: PHPStan\Rules\PHPUnit\CoversHelper - class: PHPStan\Rules\PHPUnit\AnnotationHelper + - + class: PHPStan\Rules\PHPUnit\PHPUnitVersionDetector + - class: PHPStan\Rules\PHPUnit\DataProviderHelper factory: @PHPStan\Rules\PHPUnit\DataProviderHelperFactory::create() diff --git a/phpstan.neon b/phpstan.neon index 57379453..7f64d1c0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,6 +7,8 @@ includes: - phpstan-baseline.neon parameters: + reportUnmatchedIgnoredErrors: false + excludePaths: - tests/*/data/* ignoreErrors: diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index 5f65656f..64137c76 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\PHPUnit; +use PhpParser\Comment\Doc; use PhpParser\Modifiers; use PhpParser\Node\Attribute; use PhpParser\Node\Expr\ClassConstFetch; @@ -19,25 +20,19 @@ use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; +use ReflectionMethod; use function array_merge; use function count; use function explode; +use function method_exists; use function preg_match; use function sprintf; class DataProviderHelper { - /** - * Reflection provider. - * - */ private ReflectionProvider $reflectionProvider; - /** - * The file type mapper. - * - */ private FileTypeMapper $fileTypeMapper; private Parser $parser; @@ -58,56 +53,23 @@ public function __construct( } /** + * @param ReflectionMethod|ClassMethod $node + * * @return iterable */ public function getDataProviderMethods( Scope $scope, - ClassMethod $node, + $node, ClassReflection $classReflection ): iterable { - $docComment = $node->getDocComment(); - if ($docComment !== null) { - $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $classReflection->getName(), - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $node->name->toString(), - $docComment->getText(), - ); - foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) { - $dataProviderValue = $this->getDataProviderAnnotationValue($annotation); - if ($dataProviderValue === null) { - // Missing value is already handled in NoMissingSpaceInMethodAnnotationRule - continue; - } - - $dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue); - $dataProviderMethod[] = $node->getStartLine(); - - yield $dataProviderValue => $dataProviderMethod; - } - } + yield from $this->yieldDataProviderAnnotations($node, $scope, $classReflection); if (!$this->phpunit10OrNewer) { return; } - foreach ($node->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - $dataProviderMethod = null; - if ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataprovider') { - $dataProviderMethod = $this->parseDataProviderAttribute($attr, $classReflection); - } elseif ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataproviderexternal') { - $dataProviderMethod = $this->parseDataProviderExternalAttribute($attr); - } - if ($dataProviderMethod === null) { - continue; - } - - yield from $dataProviderMethod; - } - } + yield from $this->yieldDataProviderAttributes($node, $classReflection); } /** @@ -306,4 +268,91 @@ private function parseDataProviderAttribute(Attribute $attribute, ClassReflectio ]; } + /** + * @param ReflectionMethod|ClassMethod $node + * + * @return iterable + */ + private function yieldDataProviderAttributes($node, ClassReflection $classReflection): iterable + { + if ( + $node instanceof ReflectionMethod + ) { + /** @phpstan-ignore function.alreadyNarrowedType */ + if (!method_exists($node, 'getAttributes')) { + return; + } + + foreach ($node->getAttributes('PHPUnit\Framework\Attributes\DataProvider') as $attr) { + $args = $attr->getArguments(); + if (count($args) !== 1) { + continue; + } + + $startLine = $node->getStartLine(); + if ($startLine === false) { + $startLine = -1; + } + + yield [$classReflection, $args[0], $startLine]; + } + + return; + } + + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $dataProviderMethod = null; + if ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataprovider') { + $dataProviderMethod = $this->parseDataProviderAttribute($attr, $classReflection); + } elseif ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataproviderexternal') { + $dataProviderMethod = $this->parseDataProviderExternalAttribute($attr); + } + if ($dataProviderMethod === null) { + continue; + } + + yield from $dataProviderMethod; + } + } + } + + /** + * @param ReflectionMethod|ClassMethod $node + * + * @return iterable + */ + private function yieldDataProviderAnnotations($node, Scope $scope, ClassReflection $classReflection): iterable + { + $docComment = $node->getDocComment(); + if ($docComment === null || $docComment === false) { + return; + } + + $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $node instanceof ClassMethod ? $node->name->toString() : $node->getName(), + $docComment instanceof Doc ? $docComment->getText() : $docComment, + ); + foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) { + $dataProviderValue = $this->getDataProviderAnnotationValue($annotation); + if ($dataProviderValue === null) { + // Missing value is already handled in NoMissingSpaceInMethodAnnotationRule + continue; + } + + $startLine = $node->getStartLine(); + if ($startLine === false) { + $startLine = -1; + } + + $dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue); + $dataProviderMethod[] = $startLine; + + yield $dataProviderValue => $dataProviderMethod; + } + } + } diff --git a/src/Rules/PHPUnit/DataProviderHelperFactory.php b/src/Rules/PHPUnit/DataProviderHelperFactory.php index a0768c8a..23a5f34a 100644 --- a/src/Rules/PHPUnit/DataProviderHelperFactory.php +++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php @@ -5,12 +5,6 @@ use PHPStan\Parser\Parser; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\FileTypeMapper; -use PHPUnit\Framework\TestCase; -use function dirname; -use function explode; -use function file_get_contents; -use function is_file; -use function json_decode; class DataProviderHelperFactory { @@ -21,43 +15,24 @@ class DataProviderHelperFactory private Parser $parser; + private PHPUnitVersionDetector $PHPUnitVersionDetector; + public function __construct( ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper, - Parser $parser + Parser $parser, + PHPUnitVersionDetector $PHPUnitVersionDetector ) { $this->reflectionProvider = $reflectionProvider; $this->fileTypeMapper = $fileTypeMapper; $this->parser = $parser; + $this->PHPUnitVersionDetector = $PHPUnitVersionDetector; } public function create(): DataProviderHelper { - $phpUnit10OrNewer = false; - if ($this->reflectionProvider->hasClass(TestCase::class)) { - $testCase = $this->reflectionProvider->getClass(TestCase::class); - $file = $testCase->getFileName(); - if ($file !== null) { - $phpUnitRoot = dirname($file, 3); - $phpUnitComposer = $phpUnitRoot . '/composer.json'; - if (is_file($phpUnitComposer)) { - $composerJson = @file_get_contents($phpUnitComposer); - if ($composerJson !== false) { - $json = json_decode($composerJson, true); - $version = $json['extra']['branch-alias']['dev-main'] ?? null; - if ($version !== null) { - $majorVersion = (int) explode('.', $version)[0]; - if ($majorVersion >= 10) { - $phpUnit10OrNewer = true; - } - } - } - } - } - } - - return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $this->parser, $phpUnit10OrNewer); + return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $this->parser, $this->PHPUnitVersionDetector->isPHPUnit10OrNewer()); } } diff --git a/src/Rules/PHPUnit/PHPUnitVersionDetector.php b/src/Rules/PHPUnit/PHPUnitVersionDetector.php new file mode 100644 index 00000000..841e69b9 --- /dev/null +++ b/src/Rules/PHPUnit/PHPUnitVersionDetector.php @@ -0,0 +1,57 @@ +reflectionProvider = $reflectionProvider; + } + + public function isPHPUnit10OrNewer(): bool + { + if ($this->is10OrNewer !== null) { + return $this->is10OrNewer; + } + + $this->is10OrNewer = false; + if ($this->reflectionProvider->hasClass(TestCase::class)) { + $testCase = $this->reflectionProvider->getClass(TestCase::class); + $file = $testCase->getFileName(); + if ($file !== null) { + $phpUnitRoot = dirname($file, 3); + $phpUnitComposer = $phpUnitRoot . '/composer.json'; + if (is_file($phpUnitComposer)) { + $composerJson = @file_get_contents($phpUnitComposer); + if ($composerJson !== false) { + $json = json_decode($composerJson, true); + $version = $json['extra']['branch-alias']['dev-main'] ?? null; + if ($version !== null) { + $majorVersion = (int) explode('.', $version)[0]; + if ($majorVersion >= 10) { + $this->is10OrNewer = true; + } + } + } + } + } + } + + return $this->is10OrNewer; + } + +} From 61860a671c7ab5fb3891ea730a31cf1ac669a3e9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 26 Oct 2025 09:38:27 +0100 Subject: [PATCH 256/277] Implement DataProviderDataRule --- composer.json | 2 +- extension.neon | 7 + rules.neon | 6 + src/Rules/PHPUnit/DataProviderDataRule.php | 246 +++++++++ src/Rules/PHPUnit/TestMethodsHelper.php | 94 ++++ .../PHPUnit/TestMethodsHelperFactory.php | 28 + .../PHPUnit/DataProviderDataRuleTest.php | 273 +++++++++ .../DataProviderDeclarationRuleTest.php | 7 +- .../PHPUnit/data/data-provider-data-named.php | 109 ++++ .../Rules/PHPUnit/data/data-provider-data.php | 519 ++++++++++++++++++ .../data/data-provider-trimming-args.php | 32 ++ .../data/data-provider-variadic-method.php | 61 ++ 12 files changed, 1382 insertions(+), 2 deletions(-) create mode 100644 src/Rules/PHPUnit/DataProviderDataRule.php create mode 100644 src/Rules/PHPUnit/TestMethodsHelper.php create mode 100644 src/Rules/PHPUnit/TestMethodsHelperFactory.php create mode 100644 tests/Rules/PHPUnit/DataProviderDataRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/data-provider-data-named.php create mode 100644 tests/Rules/PHPUnit/data/data-provider-data.php create mode 100644 tests/Rules/PHPUnit/data/data-provider-trimming-args.php create mode 100644 tests/Rules/PHPUnit/data/data-provider-variadic-method.php diff --git a/composer.json b/composer.json index 3c07436d..39d7a030 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.18" + "phpstan/phpstan": "^2.1.32" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/extension.neon b/extension.neon index 70831227..c6ce9bf4 100644 --- a/extension.neon +++ b/extension.neon @@ -49,9 +49,16 @@ services: class: PHPStan\Rules\PHPUnit\CoversHelper - class: PHPStan\Rules\PHPUnit\AnnotationHelper + - class: PHPStan\Rules\PHPUnit\PHPUnitVersionDetector + - + class: PHPStan\Rules\PHPUnit\TestMethodsHelper + factory: @PHPStan\Rules\PHPUnit\TestMethodsHelperFactory::create() + - + class: PHPStan\Rules\PHPUnit\TestMethodsHelperFactory + - class: PHPStan\Rules\PHPUnit\DataProviderHelper factory: @PHPStan\Rules\PHPUnit\DataProviderHelperFactory::create() diff --git a/rules.neon b/rules.neon index 84b71498..63e10b47 100644 --- a/rules.neon +++ b/rules.neon @@ -13,6 +13,9 @@ conditionalTags: PHPStan\Rules\PHPUnit\AssertEqualsIsDiscouragedRule: phpstan.rules.rule: [%strictRulesInstalled%, %featureToggles.bleedingEdge%] + PHPStan\Rules\PHPUnit\DataProviderDataRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% + services: - class: PHPStan\Rules\PHPUnit\DataProviderDeclarationRule @@ -24,3 +27,6 @@ services: - class: PHPStan\Rules\PHPUnit\AssertEqualsIsDiscouragedRule + + - + class: PHPStan\Rules\PHPUnit\DataProviderDataRule diff --git a/src/Rules/PHPUnit/DataProviderDataRule.php b/src/Rules/PHPUnit/DataProviderDataRule.php new file mode 100644 index 00000000..e241053e --- /dev/null +++ b/src/Rules/PHPUnit/DataProviderDataRule.php @@ -0,0 +1,246 @@ + + */ +class DataProviderDataRule implements Rule +{ + + private TestMethodsHelper $testMethodsHelper; + + private DataProviderHelper $dataProviderHelper; + + public function __construct( + TestMethodsHelper $testMethodsHelper, + DataProviderHelper $dataProviderHelper + ) + { + $this->testMethodsHelper = $testMethodsHelper; + $this->dataProviderHelper = $dataProviderHelper; + } + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ( + !$node instanceof Node\Stmt\Return_ + && !$node instanceof Node\Expr\Yield_ + && !$node instanceof Node\Expr\YieldFrom + ) { + return []; + } + + if ($scope->getFunction() === null) { + return []; + } + if ($scope->isInAnonymousFunction()) { + return []; + } + + $arraysTypes = $this->buildArrayTypesFromNode($node, $scope); + if ($arraysTypes === []) { + return []; + } + + $method = $scope->getFunction(); + $classReflection = $scope->getClassReflection(); + if ( + $classReflection === null + || !$classReflection->is(TestCase::class) + ) { + return []; + } + + $testsWithProvider = []; + $testMethods = $this->testMethodsHelper->getTestMethods($classReflection, $scope); + foreach ($testMethods as $testMethod) { + foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $testMethod, $classReflection) as [, $providerMethodName]) { + if ($providerMethodName === $method->getName()) { + $testsWithProvider[] = $testMethod; + continue 2; + } + } + } + + if (count($testsWithProvider) === 0) { + return []; + } + + $maxNumberOfParameters = $testsWithProvider[0]->getNumberOfParameters(); + if (count($testsWithProvider) > 1) { + foreach ($testsWithProvider as $testMethod) { + if ($testMethod->isVariadic()) { + $maxNumberOfParameters = PHP_INT_MAX; + break; + } + + $maxNumberOfParameters = max($maxNumberOfParameters, $testMethod->getNumberOfParameters()); + } + } + + foreach ($testsWithProvider as $testMethod) { + $numberOfParameters = $testMethod->getNumberOfParameters(); + + foreach ($arraysTypes as [$startLine, $arraysType]) { + $args = $this->arrayItemsToArgs($arraysType, $maxNumberOfParameters); + if ($args === null) { + continue; + } + + if ( + !$testMethod->isVariadic() + && $numberOfParameters !== $maxNumberOfParameters + ) { + $args = array_slice($args, 0, $numberOfParameters); + } + + $scope->invokeNodeCallback(new Node\Expr\MethodCall( + new TypeExpr(new ObjectType($classReflection->getName())), + $testMethod->getName(), + $args, + ['startLine' => $startLine], + )); + } + } + + return []; + } + + /** + * @return array + */ + private function arrayItemsToArgs(Type $array, int $numberOfParameters): ?array + { + $args = []; + + $constArrays = $array->getConstantArrays(); + if ($constArrays !== [] && count($constArrays) === 1) { + $keyTypes = $constArrays[0]->getKeyTypes(); + $valueTypes = $constArrays[0]->getValueTypes(); + } elseif ($array->isArray()->yes()) { + $keyTypes = []; + $valueTypes = []; + for ($i = 0; $i < $numberOfParameters; ++$i) { + $keyTypes[$i] = $array->getIterableKeyType(); + $valueTypes[$i] = $array->getIterableValueType(); + } + } else { + return null; + } + + foreach ($valueTypes as $i => $valueType) { + $key = $keyTypes[$i]->getConstantStrings(); + if (count($key) > 1) { + return null; + } + + if (count($key) === 0) { + $arg = new Node\Arg(new TypeExpr($valueType)); + $args[] = $arg; + continue; + + } + + $arg = new Node\Arg( + new TypeExpr($valueType), + false, + false, + [], + new Node\Identifier($key[0]->getValue()), + ); + $args[] = $arg; + } + + return $args; + } + + /** + * @param Node\Stmt\Return_|Node\Expr\Yield_|Node\Expr\YieldFrom $node + * + * @return list + */ + private function buildArrayTypesFromNode(Node $node, Scope $scope): array + { + $arraysTypes = []; + + // special case for providers only containing static data, so we get more precise error lines + if ( + ($node instanceof Node\Stmt\Return_ && $node->expr instanceof Node\Expr\Array_) + || ($node instanceof Node\Expr\YieldFrom && $node->expr instanceof Node\Expr\Array_) + ) { + foreach ($node->expr->items as $item) { + if (!$item->value instanceof Node\Expr\Array_) { + $arraysTypes = []; + break; + } + + $constArrays = $scope->getType($item->value)->getConstantArrays(); + if ($constArrays === []) { + $arraysTypes = []; + break; + } + + foreach ($constArrays as $constArray) { + $arraysTypes[] = [$item->value->getStartLine(), $constArray]; + } + } + + if ($arraysTypes !== []) { + return $arraysTypes; + } + } + + // general case with less precise error message lines + if ($node instanceof Node\Stmt\Return_ || $node instanceof Node\Expr\YieldFrom) { + if ($node->expr === null) { + return []; + } + + $exprType = $scope->getType($node->expr); + $exprConstArrays = $exprType->getConstantArrays(); + foreach ($exprConstArrays as $constArray) { + foreach ($constArray->getValueTypes() as $valueType) { + foreach ($valueType->getConstantArrays() as $constValueArray) { + $arraysTypes[] = [$node->getStartLine(), $constValueArray]; + } + } + } + + if ($arraysTypes === []) { + foreach ($exprType->getIterableValueType()->getArrays() as $arrayType) { + $arraysTypes[] = [$node->getStartLine(), $arrayType]; + } + } + } elseif ($node instanceof Node\Expr\Yield_) { + if ($node->value === null) { + return []; + } + + $exprType = $scope->getType($node->value); + foreach ($exprType->getConstantArrays() as $constValueArray) { + $arraysTypes[] = [$node->getStartLine(), $constValueArray]; + } + } + + return $arraysTypes; + } + +} diff --git a/src/Rules/PHPUnit/TestMethodsHelper.php b/src/Rules/PHPUnit/TestMethodsHelper.php new file mode 100644 index 00000000..d0984442 --- /dev/null +++ b/src/Rules/PHPUnit/TestMethodsHelper.php @@ -0,0 +1,94 @@ +fileTypeMapper = $fileTypeMapper; + $this->phpunit10OrNewer = $phpunit10OrNewer; + } + + /** + * @return array + */ + public function getTestMethods(ClassReflection $classReflection, Scope $scope): array + { + $testMethods = []; + foreach ($classReflection->getNativeReflection()->getMethods() as $reflectionMethod) { + if (!$reflectionMethod->isPublic()) { + continue; + } + + if (str_starts_with(strtolower($reflectionMethod->getName()), 'test')) { + $testMethods[] = $reflectionMethod; + continue; + } + + $docComment = $reflectionMethod->getDocComment(); + if ($docComment !== false) { + $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $reflectionMethod->getName(), + $docComment, + ); + + if ($this->hasTestAnnotation($methodPhpDoc)) { + $testMethods[] = $reflectionMethod; + continue; + } + } + + if (!$this->phpunit10OrNewer) { + continue; + } + + $testAttributes = $reflectionMethod->getAttributes('PHPUnit\Framework\Attributes\Test'); // @phpstan-ignore argument.type + if ($testAttributes === []) { + continue; + } + + $testMethods[] = $reflectionMethod; + } + + return $testMethods; + } + + private function hasTestAnnotation(?ResolvedPhpDocBlock $phpDoc): bool + { + if ($phpDoc === null) { + return false; + } + + $phpDocNodes = $phpDoc->getPhpDocNodes(); + + foreach ($phpDocNodes as $docNode) { + $tags = $docNode->getTagsByName('@test'); + if ($tags !== []) { + return true; + } + } + + return false; + } + +} diff --git a/src/Rules/PHPUnit/TestMethodsHelperFactory.php b/src/Rules/PHPUnit/TestMethodsHelperFactory.php new file mode 100644 index 00000000..202f88cb --- /dev/null +++ b/src/Rules/PHPUnit/TestMethodsHelperFactory.php @@ -0,0 +1,28 @@ +fileTypeMapper = $fileTypeMapper; + $this->PHPUnitVersionDetector = $PHPUnitVersionDetector; + } + + public function create(): TestMethodsHelper + { + return new TestMethodsHelper($this->fileTypeMapper, $this->PHPUnitVersionDetector->isPHPUnit10OrNewer()); + } + +} diff --git a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php new file mode 100644 index 00000000..fa3aa9fc --- /dev/null +++ b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php @@ -0,0 +1,273 @@ + + */ +class DataProviderDataRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + /** @var list> $rules */ + $rules = [ + new DataProviderDataRule( + new TestMethodsHelper( + self::getContainer()->getByType(FileTypeMapper::class), + true + ), + new DataProviderHelper( + $reflectionProvider, + self::getContainer()->getByType(FileTypeMapper::class), + self::getContainer()->getService('defaultAnalysisParser'), + true + ), + + ), + self::getContainer()->getByType(CallMethodsRule::class) /** @phpstan-ignore phpstanApi.classConstant */ + ]; + + return new CompositeRule($rules); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/data-provider-data.php'], [ + [ + 'Parameter #2 $input of method DataProviderDataTest\FooTest::testWithAttribute() expects string, int given.', + 24, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\FooTest::testWithAttribute() expects string, false given.', + 28, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\BarTest::testWithAnnotation() expects string, int given.', + 51, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\BarTest::testWithAnnotation() expects string, false given.', + 55, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\YieldTest::myTestMethod() expects string, int given.', + 80, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\YieldTest::myTestMethod() expects string, false given.', + 86, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\YieldFromTest::myTestMethod() expects string, int given.', + 112, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\YieldFromTest::myTestMethod() expects string, false given.', + 116, + ], + [ + 'Method DataProviderDataTest\DifferentArgumentCount::testFoo() invoked with 3 parameters, 2 required.', + 141, + ], + [ + 'Method DataProviderDataTest\DifferentArgumentCount::testFoo() invoked with 1 parameter, 2 required.', + 146, + ], + [ + 'Method DataProviderDataTest\DifferentArgumentCountWithReusedDataprovider::testFoo() invoked with 3 parameters, 2 required.', + 177, + ], + [ + 'Method DataProviderDataTest\DifferentArgumentCountWithReusedDataprovider::testFoo() invoked with 1 parameter, 2 required.', + 182, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\UnionTypeReturnTest::testFoo() expects string, int given.', + 216, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\YieldFromExpr::testFoo() expects string, int given.', + 236, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\YieldFromExpr::testFoo() expects string, true given.', + 238, + ], + [ + 'Parameter #1 $si of method DataProviderDataTest\TestInvalidVariadic::testBar() expects int, string given.', + 295, + ], + [ + 'Parameter #1 $s of method DataProviderDataTest\TestInvalidVariadic::testFoo() expects string, int given.', + 296, + ], + [ + 'Parameter #1 $si of method DataProviderDataTest\TestInvalidVariadic2::testBar() expects int, string given.', + 317, + ], + [ + 'Parameter #2 ...$moreS of method DataProviderDataTest\TestInvalidVariadic2::testFoo() expects int, string given.', + 317, + ], + [ + 'Parameter #4 ...$moreS of method DataProviderDataTest\TestInvalidVariadic2::testFoo() expects int, string given.', + 317, + ], + [ + 'Parameter #1 $s of method DataProviderDataTest\TestInvalidVariadic2::testFoo() expects string, int given.', + 318, + ], + [ + 'Parameter #1 $i of method DataProviderDataTest\TestArrayIterator::testBar() expects int, int|string given.', + 362, + ], + [ + 'Parameter #1 $i of method DataProviderDataTest\TestArrayIterator::testFoo() expects int, int|string given.', + 362, + ], + [ + 'Parameter #1 $s1 of method DataProviderDataTest\TestArrayIterator::testFooBar() expects string, int|string given.', + 362, + ], + [ + 'Parameter #1 $si of method DataProviderDataTest\TestWrongTypedIterable::testBar() expects int, string given.', + 380, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\AbstractBaseTest::testWithAttribute() expects string, int given.', + 407, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\AbstractBaseTest::testWithAttribute() expects string, false given.', + 411, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\ConstantArrayUnionTypeReturnTest::testFoo() expects string, int given.', + 446, + ], + [ + 'Method DataProviderDataTest\ConstantArrayDifferentLengthUnionTypeReturnTest::testFoo() invoked with 3 parameters, 2 required.', + 484, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\ConstantArrayDifferentLengthUnionTypeReturnTest::testFoo() expects string, int given.', + 484, + ], + [ + 'Parameter #2 $input of method DataProviderDataTest\ConstantArrayUnionWithDifferentValueTypeReturnTest::testFoo() expects string, int|string given.', + 517, + ], + ]); + } + + public function testRulePhp8(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped(); + } + + $this->analyse([__DIR__ . '/data/data-provider-data-named.php'], [ + [ + 'Parameter $input of method DataProviderDataTestPhp8\NamedArgsInProvider::testFoo() expects string, int given.', + 44 + ], + [ + 'Parameter $input of method DataProviderDataTestPhp8\NamedArgsInProvider::testFoo() expects string, false given.', + 44 + ], + [ + 'Unknown parameter $wrong in call to method DataProviderDataTestPhp8\TestWrongOffsetNameArrayShapeIterable::testBar().', + 58 + ], + [ + 'Missing parameter $si (int) in call to method DataProviderDataTestPhp8\TestWrongOffsetNameArrayShapeIterable::testBar().', + 58 + ], + [ + 'Parameter $si of method DataProviderDataTestPhp8\TestWrongTypeInArrayShapeIterable::testBar() expects int, string given.', + 79 + ], + ]); + } + + + public function testVariadicMethod(): void + { + $this->analyse([__DIR__ . '/data/data-provider-variadic-method.php'], [ + [ + 'Method DataProviderVariadicMethod\FooTest::testProvide2() invoked with 1 parameter, at least 2 required.', + 12, + ], + [ + 'Parameter #1 $a of method DataProviderVariadicMethod\FooTest::testProvide() expects int, string given.', + 13, + ], + [ + 'Method DataProviderVariadicMethod\FooTest::testProvide2() invoked with 1 parameter, at least 2 required.', + 13, + ], + [ + 'Parameter #1 $a of method DataProviderVariadicMethod\FooTest::testProvide2() expects int, string given.', + 13, + ], + [ + 'Parameter #2 ...$rest of method DataProviderVariadicMethod\FooTest::testProvide() expects string, int given.', + 15, + ], + [ + 'Parameter #3 ...$rest of method DataProviderVariadicMethod\FooTest::testProvide() expects string, int given.', + 15, + ], + [ + 'Parameter #2 $two of method DataProviderVariadicMethod\FooTest::testProvide2() expects string, int given.', + 15, + ], + [ + 'Parameter #3 ...$rest of method DataProviderVariadicMethod\FooTest::testProvide2() expects string, int given.', + 15, + ], + ]); + } + + public function testTrimmingArgs(): void + { + $this->analyse([__DIR__ . '/data/data-provider-trimming-args.php'], [ + [ + 'Method DataProviderTrimmingArgs\FooTest::testProvide() invoked with 2 parameters, 1 required.', + 12, + ], + [ + 'Method DataProviderTrimmingArgs\FooTest::testProvide2() invoked with 2 parameters, 1 required.', + 12, + ], + [ + 'Method DataProviderTrimmingArgs\FooTest::testProvide() invoked with 2 parameters, 1 required.', + 13, + ], + [ + 'Method DataProviderTrimmingArgs\FooTest::testProvide2() invoked with 2 parameters, 1 required.', + 13, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } +} diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index cbe40bf1..67f55ed4 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -17,7 +17,12 @@ protected function getRule(): Rule $reflection = $this->createReflectionProvider(); return new DataProviderDeclarationRule( - new DataProviderHelper($reflection, self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getService('defaultAnalysisParser'), true), + new DataProviderHelper( + $reflection, + self::getContainer()->getByType(FileTypeMapper::class), + self::getContainer()->getService('defaultAnalysisParser'), + true + ), true, true ); diff --git a/tests/Rules/PHPUnit/data/data-provider-data-named.php b/tests/Rules/PHPUnit/data/data-provider-data-named.php new file mode 100644 index 00000000..bea3e50e --- /dev/null +++ b/tests/Rules/PHPUnit/data/data-provider-data-named.php @@ -0,0 +1,109 @@ + 'Hello World', + "expectedResult" => " Hello World \n" + ] + ]; + + if (rand(0,1)) { + $arr = [ + [ + "input" => 123, + "expectedResult" => " Hello World \n" + ] + ]; + } + if (rand(0,1)) { + $arr = [ + [ + "input" => false, + "expectedResult" => " Hello World \n" + ] + ]; + } + + return $arr; + } +} + + +class TestWrongOffsetNameArrayShapeIterable extends TestCase +{ + /** @dataProvider aProvider */ + public function testBar(int $si): void + { + } + + public function aProvider(): iterable + { + return $this->data(); + } + + /** + * @return iterable + */ + public function data(): iterable + { + } +} + + +class TestWrongTypeInArrayShapeIterable extends TestCase +{ + /** @dataProvider aProvider */ + public function testBar(int $si): void + { + } + + public function aProvider(): iterable + { + return $this->data(); + } + + /** + * @return iterable + */ + public function data(): iterable + { + } +} + + +class TestValidArrayShapeIterable extends TestCase +{ + /** @dataProvider aProvider */ + public function testBar(int $si): void + { + } + + public function aProvider(): iterable + { + return $this->data(); + } + + /** + * @return iterable + */ + public function data(): iterable + { + } +} diff --git a/tests/Rules/PHPUnit/data/data-provider-data.php b/tests/Rules/PHPUnit/data/data-provider-data.php new file mode 100644 index 00000000..d684df79 --- /dev/null +++ b/tests/Rules/PHPUnit/data/data-provider-data.php @@ -0,0 +1,519 @@ +moreData(); + + yield [ + 'Hello World', + true, + ]; + } + + /** + * @return array{array{'Hello World', 123}} + */ + private function moreData(): array + { + return [ + [ + 'Hello World', + 123, + ] + ]; + } +} + +class TestValidVariadic extends TestCase +{ + /** @dataProvider aProvider */ + public function testBar(string $s): void + { + } + + /** @dataProvider aProvider */ + public function testFoo(string $s, string ...$moreS): void + { + } + + public function aProvider(): iterable + { + return [ + ["hello", "world", "foo", "bar"], + ["hi", "ho"], + ["nope"] + ]; + } +} + +class TestInvalidVariadic extends TestCase +{ + /** @dataProvider aProvider */ + public function testBar(int $si): void + { + } + + /** @dataProvider aProvider */ + public function testFoo(string $s, string ...$moreS): void + { + } + + public function aProvider(): iterable + { + return [ + ["hello", "world", "foo", "bar"], + [123] + ]; + } +} + + +class TestInvalidVariadic2 extends TestCase +{ + /** @dataProvider aProvider */ + public function testBar(int $si): void + { + } + + /** @dataProvider aProvider */ + public function testFoo(string $s, int ...$moreS): void + { + } + + public function aProvider(): iterable + { + return [ + ["hello", "world", 5, "bar"], + [123] + ]; + } +} + +class TestTypedIterable extends TestCase +{ + /** @dataProvider aProvider */ + public function testBar(int $si): void + { + } + + public function aProvider(): iterable + { + return $this->data(); + } + + /** + * @return iterable> + */ + public function data(): iterable + { + } +} + +class TestArrayIterator extends TestCase +{ + /** @dataProvider aProvider */ + public function testBar(int $i): void + { + } + + /** @dataProvider aProvider */ + public function testFoo(int $i, string $si): void + { + } + + /** @dataProvider aProvider */ + public function testFooBar(string $s1, string $s2): void + { + } + + public function aProvider(): iterable + { + return new \ArrayIterator([ + [1], + [2, "hello"], + ["no"], + ["no", "yes"], + ]); + } +} + +class TestWrongTypedIterable extends TestCase +{ + /** @dataProvider aProvider */ + public function testBar(int $si): void + { + } + + public function aProvider(): iterable + { + return $this->data(); + } + + /** + * @return iterable> + */ + public function data(): iterable + { + } +} + + +abstract class AbstractBaseTest extends TestCase +{ + + #[DataProvider('aProvider')] + public function testWithAttribute(string $expectedResult, string $input): void + { + } + + static public function aProvider(): array + { + return [ + [ + 'Hello World', + " Hello World \n", + ], + [ + 'Hello World', + 123, + ], + [ + 'Hello World', + false, + ], + ]; + } +} + + +class ConstantArrayUnionTypeReturnTest extends TestCase +{ + + /** @dataProvider aProvider */ + public function testFoo(string $expectedResult, string $input): void + { + } + + public function aProvider(): array + { + if (rand(0,1)) { + $arr = [ + [ + 'Hello World', + 123 + ] + ]; + } else { + $arr = [ + [ + 'Hello World', + " Hello World \n" + ] + ]; + } + + return $arr; + } +} + +class ConstantArrayDifferentLengthUnionTypeReturnTest extends TestCase +{ + + /** @dataProvider aProvider */ + public function testFoo(string $expectedResult, string $input): void + { + } + + public function aProvider(): array + { + if (rand(0,1)) { + $arr = [ + [ + 'Hello World', + 123 + ] + ]; + } elseif (rand(0,1)) { + $arr = [ + [ + 'Hello World', + 'Hello World', + ] + ]; + } else { + $arr = [ + [ + 'Hello World', + " Hello World \n", + " Too much \n", + ] + ]; + } + + return $arr; + } +} + +class ConstantArrayUnionWithDifferentValueTypeReturnTest extends TestCase +{ + + /** @dataProvider aProvider */ + public function testFoo(string $expectedResult, string $input): void + { + } + + public function aProvider(): array + { + if (rand(0,1)) { + $arr = [ + [ + 'Hellooo', + ' World', + ] + ]; + } else { + $a = rand(0,1) ? 'Hello' : 'World'; + $b = rand(0,1) ? " Hello World \n" : 123; + + $arr = [ + [ + $a, + $b + ] + ]; + } + + return $arr; + } +} diff --git a/tests/Rules/PHPUnit/data/data-provider-trimming-args.php b/tests/Rules/PHPUnit/data/data-provider-trimming-args.php new file mode 100644 index 00000000..60fc7cf4 --- /dev/null +++ b/tests/Rules/PHPUnit/data/data-provider-trimming-args.php @@ -0,0 +1,32 @@ + Date: Sun, 26 Oct 2025 17:08:45 +0100 Subject: [PATCH 257/277] Fix memory consumption in DataProviderDataRule --- src/Rules/PHPUnit/DataProviderDataRule.php | 23 ++++++++----- .../data/data-provider-trimming-args.php | 34 +++++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/Rules/PHPUnit/DataProviderDataRule.php b/src/Rules/PHPUnit/DataProviderDataRule.php index e241053e..45a7fa16 100644 --- a/src/Rules/PHPUnit/DataProviderDataRule.php +++ b/src/Rules/PHPUnit/DataProviderDataRule.php @@ -84,15 +84,20 @@ public function processNode(Node $node, Scope $scope): array return []; } - $maxNumberOfParameters = $testsWithProvider[0]->getNumberOfParameters(); - if (count($testsWithProvider) > 1) { - foreach ($testsWithProvider as $testMethod) { - if ($testMethod->isVariadic()) { - $maxNumberOfParameters = PHP_INT_MAX; - break; - } + $maxNumberOfParameters = null; + foreach ($testsWithProvider as $testMethod) { + $num = $testMethod->getNumberOfParameters(); + if ($testMethod->isVariadic()) { + $num = PHP_INT_MAX; + } + if ($maxNumberOfParameters === null) { + $maxNumberOfParameters = $num; + continue; + } - $maxNumberOfParameters = max($maxNumberOfParameters, $testMethod->getNumberOfParameters()); + $maxNumberOfParameters = max($maxNumberOfParameters, $num); + if ($num === PHP_INT_MAX) { + break; } } @@ -100,7 +105,7 @@ public function processNode(Node $node, Scope $scope): array $numberOfParameters = $testMethod->getNumberOfParameters(); foreach ($arraysTypes as [$startLine, $arraysType]) { - $args = $this->arrayItemsToArgs($arraysType, $maxNumberOfParameters); + $args = $this->arrayItemsToArgs($arraysType, $numberOfParameters); if ($args === null) { continue; } diff --git a/tests/Rules/PHPUnit/data/data-provider-trimming-args.php b/tests/Rules/PHPUnit/data/data-provider-trimming-args.php index 60fc7cf4..231d3f52 100644 --- a/tests/Rules/PHPUnit/data/data-provider-trimming-args.php +++ b/tests/Rules/PHPUnit/data/data-provider-trimming-args.php @@ -30,3 +30,37 @@ public function testProvide2(int $i): void } } + +class BarTest extends TestCase +{ + + /** + * @return array> + */ + public function getData(): array + { + return []; + } + + public function dataProvide(): array + { + return $this->getData(); + } + + /** + * @dataProvider dataProvide + */ + public function testProvide(string ...$arg): void + { + + } + + /** + * @dataProvider dataProvide + */ + public function testProvide2(string $arg): void + { + + } + +} From 12676a75bce0124ed355e6dd189ccd996cdcbbe8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 26 Oct 2025 20:46:49 +0100 Subject: [PATCH 258/277] One more test --- .../PHPUnit/DataProviderDataRuleTest.php | 4 +++ .../data/data-provider-trimming-args.php | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php index fa3aa9fc..647f830f 100644 --- a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php @@ -258,6 +258,10 @@ public function testTrimmingArgs(): void 'Method DataProviderTrimmingArgs\FooTest::testProvide2() invoked with 2 parameters, 1 required.', 13, ], + [ + 'Parameter #6 ...$m of method DataProviderTrimmingArgs\BazTest::testProvide() expects int, string given.', + 90, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/data-provider-trimming-args.php b/tests/Rules/PHPUnit/data/data-provider-trimming-args.php index 231d3f52..dcfcd84d 100644 --- a/tests/Rules/PHPUnit/data/data-provider-trimming-args.php +++ b/tests/Rules/PHPUnit/data/data-provider-trimming-args.php @@ -64,3 +64,31 @@ public function testProvide2(string $arg): void } } + +class BazTest extends TestCase +{ + + /** + * @dataProvider dataProvide + */ + public function testProvide(int $i, int $j, int $k, int ...$m): void + { + + } + + /** + * @dataProvider dataProvide + */ + public function testProvide2(int $i, int $j, int $k, int $m, int $n): void + { + + } + + public function dataProvide(): array + { + return [ + [1, 2, 3, 4, 5, 'foo'], + ]; + } + +} From 53e33fc365ad23de4fbbde1acab40ea36980b64d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 26 Oct 2025 21:26:50 +0100 Subject: [PATCH 259/277] Setup mutation testing --- .github/workflows/build.yml | 68 +++++++++++++++++++++++++++++++++++++ phpstan.neon | 2 ++ phpunit.xml | 3 +- 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28ab39c6..f4f92b51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,10 @@ on: branches: - "2.0.x" +concurrency: + group: build-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + jobs: lint: name: "Lint" @@ -213,3 +217,67 @@ jobs: - name: "PHPStan" run: "make phpstan" + + mutation-testing: + name: "Mutation Testing" + runs-on: "ubuntu-latest" + needs: ["tests", "static-analysis"] + + strategy: + fail-fast: false + matrix: + php-version: + - "8.2" + - "8.3" + - "8.4" + operating-system: [ubuntu-latest] + + steps: + - name: "Checkout" + uses: actions/checkout@v5 + + - name: "Checkout build-infection" + uses: actions/checkout@v5 + with: + repository: "phpstan/build-infection" + path: "build-infection" + ref: "1.x" + + - uses: ./build-infection/.github/actions/setup-php + with: + php-version: "${{ matrix.php-version }}" + extensions: mbstring + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Install build-infection dependencies" + working-directory: "build-infection" + run: "composer install --no-interaction --no-progress" + + - name: "Configure infection" + run: | + php build-infection/bin/infection-config.php \ + > infection.json5 + cat infection.json5 | jq + + - name: "Cache Result cache" + uses: actions/cache@v4 + with: + path: ./tmp + key: "result-cache-v1-${{ matrix.php-version }}-${{ github.run_id }}" + restore-keys: | + result-cache-v1-${{ matrix.php-version }}- + + - name: "Run infection" + run: | + git fetch --depth=1 origin $GITHUB_BASE_REF + infection \ + --git-diff-base=origin/$GITHUB_BASE_REF \ + --git-diff-lines \ + --ignore-msi-with-no-mutations \ + --min-msi=100 \ + --min-covered-msi=100 \ + --log-verbosity=all \ + --debug \ + --logger-text=php://stdout diff --git a/phpstan.neon b/phpstan.neon index 7f64d1c0..a88579c4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,6 +9,8 @@ includes: parameters: reportUnmatchedIgnoredErrors: false + resultCachePath: tmp/resultCache.php + excludePaths: - tests/*/data/* ignoreErrors: diff --git a/phpunit.xml b/phpunit.xml index 420ef740..2ccef9fc 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,7 +10,8 @@ beStrictAboutTodoAnnotatedTests="true" failOnRisky="true" failOnWarning="true" - xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xml" + xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" + executionOrder="random" > From 11f3ada682f2e260c5ff422de17fefbbe8ee0496 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 07:07:13 +0100 Subject: [PATCH 260/277] Fix mutation testing on dev branch (#244) --- .github/workflows/build.yml | 20 ++++++++++++++++---- Makefile | 4 ++-- phpstan.neon | 1 + 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4f92b51..50334926 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -261,8 +261,13 @@ jobs: > infection.json5 cat infection.json5 | jq - - name: "Cache Result cache" - uses: actions/cache@v4 + - name: "Determine default branch" + id: default-branch + run: | + echo "name=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p')" >> $GITHUB_OUTPUT + + - name: "Restore result cache" + uses: actions/cache/restore@v4 with: path: ./tmp key: "result-cache-v1-${{ matrix.php-version }}-${{ github.run_id }}" @@ -271,9 +276,9 @@ jobs: - name: "Run infection" run: | - git fetch --depth=1 origin $GITHUB_BASE_REF + git fetch --depth=1 origin ${{ steps.default-branch.outputs.name }} infection \ - --git-diff-base=origin/$GITHUB_BASE_REF \ + --git-diff-base=origin/${{ steps.default-branch.outputs.name }} \ --git-diff-lines \ --ignore-msi-with-no-mutations \ --min-msi=100 \ @@ -281,3 +286,10 @@ jobs: --log-verbosity=all \ --debug \ --logger-text=php://stdout + + - name: "Save result cache" + uses: actions/cache/save@v4 + if: ${{ !cancelled() }} + with: + path: ./tmp + key: "result-cache-v1-${{ matrix.php-version }}-${{ github.run_id }}" diff --git a/Makefile b/Makefile index 1ee557df..8cec0a5b 100644 --- a/Makefile +++ b/Makefile @@ -26,8 +26,8 @@ cs-fix: .PHONY: phpstan phpstan: - php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests + php vendor/bin/phpstan analyse -c phpstan.neon src tests .PHONY: phpstan-generate-baseline phpstan-generate-baseline: - php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests -b phpstan-baseline.neon + php vendor/bin/phpstan analyse -c phpstan.neon src tests -b phpstan-baseline.neon diff --git a/phpstan.neon b/phpstan.neon index a88579c4..4441aa97 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,6 +7,7 @@ includes: - phpstan-baseline.neon parameters: + level: 8 reportUnmatchedIgnoredErrors: false resultCachePath: tmp/resultCache.php From 1fbc321eb8f4ddd719f9f31348edcc54510c5e94 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 07:23:29 +0100 Subject: [PATCH 261/277] move analyse-paths into phpstan.neon --- Makefile | 4 ++-- phpstan.neon | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8cec0a5b..c58ca06e 100644 --- a/Makefile +++ b/Makefile @@ -26,8 +26,8 @@ cs-fix: .PHONY: phpstan phpstan: - php vendor/bin/phpstan analyse -c phpstan.neon src tests + php vendor/bin/phpstan analyse -c phpstan.neon .PHONY: phpstan-generate-baseline phpstan-generate-baseline: - php vendor/bin/phpstan analyse -c phpstan.neon src tests -b phpstan-baseline.neon + php vendor/bin/phpstan analyse -c phpstan.neon -b phpstan-baseline.neon diff --git a/phpstan.neon b/phpstan.neon index 4441aa97..7b35ce80 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -12,6 +12,10 @@ parameters: resultCachePath: tmp/resultCache.php + paths: + - src + - tests + excludePaths: - tests/*/data/* ignoreErrors: From 5c89d749a03a8efc333b097407ba9f99692f589f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 12:04:54 +0100 Subject: [PATCH 262/277] Ignore missingType.iterableValue for data-providers --- extension.neon | 7 +++ rules.neon | 2 +- src/Rules/PHPUnit/DataProviderDataRule.php | 6 +- src/Rules/PHPUnit/DataProviderHelper.php | 8 +-- src/Rules/PHPUnit/TestMethodsHelper.php | 5 ++ .../DataProviderReturnTypeIgnoreExtension.php | 56 +++++++++++++++++++ ...aProviderReturnTypeIgnoreExtensionTest.php | 38 +++++++++++++ .../data/data-provider-iterable-value.neon | 6 ++ .../data/data-provider-iterable-value.php | 39 +++++++++++++ 9 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php create mode 100644 tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php create mode 100644 tests/Type/PHPUnit/data/data-provider-iterable-value.neon create mode 100644 tests/Type/PHPUnit/data/data-provider-iterable-value.php diff --git a/extension.neon b/extension.neon index c6ce9bf4..dcf43818 100644 --- a/extension.neon +++ b/extension.neon @@ -1,6 +1,7 @@ parameters: phpunit: convertUnionToIntersectionType: true + checkDataProviderData: %featureToggles.bleedingEdge% additionalConstructors: - PHPUnit\Framework\TestCase::setUp earlyTerminatingMethodCalls: @@ -24,6 +25,7 @@ parameters: parametersSchema: phpunit: structure([ convertUnionToIntersectionType: bool() + checkDataProviderData: bool(), ]) services: @@ -67,6 +69,11 @@ services: arguments: parser: @defaultAnalysisParser + - + class: PHPStan\Type\PHPUnit\DataProviderReturnTypeIgnoreExtension + conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: phpstan.phpDoc.typeNodeResolverExtension: %phpunit.convertUnionToIntersectionType% + PHPStan\Type\PHPUnit\DataProviderReturnTypeIgnoreExtension: + phpstan.ignoreErrorExtension: %phpunit.checkDataProviderData% diff --git a/rules.neon b/rules.neon index 63e10b47..8272f47a 100644 --- a/rules.neon +++ b/rules.neon @@ -14,7 +14,7 @@ conditionalTags: phpstan.rules.rule: [%strictRulesInstalled%, %featureToggles.bleedingEdge%] PHPStan\Rules\PHPUnit\DataProviderDataRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% + phpstan.rules.rule: %phpunit.checkDataProviderData% services: - diff --git a/src/Rules/PHPUnit/DataProviderDataRule.php b/src/Rules/PHPUnit/DataProviderDataRule.php index 45a7fa16..6f250fee 100644 --- a/src/Rules/PHPUnit/DataProviderDataRule.php +++ b/src/Rules/PHPUnit/DataProviderDataRule.php @@ -8,7 +8,6 @@ use PHPStan\Rules\Rule; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPUnit\Framework\TestCase; use function array_slice; use function count; use function max; @@ -62,10 +61,7 @@ public function processNode(Node $node, Scope $scope): array $method = $scope->getFunction(); $classReflection = $scope->getClassReflection(); - if ( - $classReflection === null - || !$classReflection->is(TestCase::class) - ) { + if ($classReflection === null) { return []; } diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index 64137c76..b468dfcb 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -53,23 +53,23 @@ public function __construct( } /** - * @param ReflectionMethod|ClassMethod $node + * @param ReflectionMethod|ClassMethod $testMethod * * @return iterable */ public function getDataProviderMethods( Scope $scope, - $node, + $testMethod, ClassReflection $classReflection ): iterable { - yield from $this->yieldDataProviderAnnotations($node, $scope, $classReflection); + yield from $this->yieldDataProviderAnnotations($testMethod, $scope, $classReflection); if (!$this->phpunit10OrNewer) { return; } - yield from $this->yieldDataProviderAttributes($node, $classReflection); + yield from $this->yieldDataProviderAttributes($testMethod, $classReflection); } /** diff --git a/src/Rules/PHPUnit/TestMethodsHelper.php b/src/Rules/PHPUnit/TestMethodsHelper.php index d0984442..67b888e7 100644 --- a/src/Rules/PHPUnit/TestMethodsHelper.php +++ b/src/Rules/PHPUnit/TestMethodsHelper.php @@ -6,6 +6,7 @@ use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\ClassReflection; use PHPStan\Type\FileTypeMapper; +use PHPUnit\Framework\TestCase; use ReflectionMethod; use function str_starts_with; use function strtolower; @@ -31,6 +32,10 @@ public function __construct( */ public function getTestMethods(ClassReflection $classReflection, Scope $scope): array { + if (!$classReflection->is(TestCase::class)) { + return []; + } + $testMethods = []; foreach ($classReflection->getNativeReflection()->getMethods() as $reflectionMethod) { if (!$reflectionMethod->isPublic()) { diff --git a/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php b/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php new file mode 100644 index 00000000..be6af678 --- /dev/null +++ b/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php @@ -0,0 +1,56 @@ +testMethodsHelper = $testMethodsHelper; + $this->dataProviderHelper = $dataProviderHelper; + } + + public function shouldIgnore(Error $error, Node $node, Scope $scope): bool + { + if ($error->getIdentifier() !== 'missingType.iterableValue') { + return false; + } + + if (!$scope->isInClass()) { + return false; + } + $classReflection = $scope->getClassReflection(); + + $methodReflection = $scope->getFunction(); + if ($methodReflection === null) { + return false; + } + + $testMethods = $this->testMethodsHelper->getTestMethods($classReflection, $scope); + foreach ($testMethods as $testMethod) { + foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $testMethod, $classReflection) as [, $providerMethodName]) { + if ($providerMethodName === $methodReflection->getName()) { + return true; + } + } + } + + return false; + } + +} diff --git a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php new file mode 100644 index 00000000..fb5b927a --- /dev/null +++ b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php @@ -0,0 +1,38 @@ + + */ +class DataProviderReturnTypeIgnoreExtensionTest extends RuleTestCase { + protected function getRule(): Rule + { + /** @phpstan-ignore phpstanApi.classConstant */ + $rule = self::getContainer()->getByType(MissingMethodReturnTypehintRule::class); + + return $rule; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/data-provider-iterable-value.php'], [ + [ + 'Method DataProviderIterableValueTest\Foo::notADataProvider() return type has no value type specified in iterable type iterable.', + 32, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type' + ], + ]); + } + + static public function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/data-provider-iterable-value.neon' + ]; + } +} diff --git a/tests/Type/PHPUnit/data/data-provider-iterable-value.neon b/tests/Type/PHPUnit/data/data-provider-iterable-value.neon new file mode 100644 index 00000000..eed12a5b --- /dev/null +++ b/tests/Type/PHPUnit/data/data-provider-iterable-value.neon @@ -0,0 +1,6 @@ +parameters: + phpunit: + checkDataProviderData: true + +includes: + - ../../../../extension.neon diff --git a/tests/Type/PHPUnit/data/data-provider-iterable-value.php b/tests/Type/PHPUnit/data/data-provider-iterable-value.php new file mode 100644 index 00000000..613d3b14 --- /dev/null +++ b/tests/Type/PHPUnit/data/data-provider-iterable-value.php @@ -0,0 +1,39 @@ + Date: Tue, 28 Oct 2025 07:28:21 +0100 Subject: [PATCH 263/277] Assert*Rules: Do cheap checks first (#247) --- src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php | 13 ++++++++----- src/Rules/PHPUnit/AssertRuleHelper.php | 3 --- src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 12 +++++++----- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 12 +++++++----- src/Rules/PHPUnit/AssertSameWithCountRule.php | 13 +++++++------ 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php index b03f0830..c4c23887 100644 --- a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php +++ b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php @@ -27,17 +27,16 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { + if (!$node instanceof Node\Expr\MethodCall && ! $node instanceof Node\Expr\StaticCall) { return []; } - - if ($node->isFirstClassCallable()) { + if (count($node->getArgs()) < 2) { return []; } - - if (count($node->getArgs()) < 2) { + if ($node->isFirstClassCallable()) { return []; } + if ( !$node->name instanceof Node\Identifier || !in_array(strtolower($node->name->name), ['assertequals', 'assertnotequals'], true) @@ -45,6 +44,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { + return []; + } + $leftType = TypeCombinator::removeNull($scope->getType($node->getArgs()[0]->value)); $rightType = TypeCombinator::removeNull($scope->getType($node->getArgs()[1]->value)); diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index 442b0655..ecaec91d 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -11,9 +11,6 @@ class AssertRuleHelper { - /** - * @phpstan-assert-if-true Node\Expr\MethodCall|Node\Expr\StaticCall $node - */ public static function isMethodOrStaticCallOnAssert(Node $node, Scope $scope): bool { if ($node instanceof Node\Expr\MethodCall) { diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 9abbd75a..fd76f7c4 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -23,15 +23,13 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { + if (!$node instanceof Node\Expr\MethodCall && ! $node instanceof Node\Expr\StaticCall) { return []; } - - if ($node->isFirstClassCallable()) { + if (count($node->getArgs()) < 2) { return []; } - - if (count($node->getArgs()) < 2) { + if ($node->isFirstClassCallable()) { return []; } if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { @@ -43,6 +41,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { + return []; + } + if ($expectedArgumentValue->name->toLowerString() === 'true') { return [ RuleErrorBuilder::message('You should use assertTrue() instead of assertSame() when expecting "true"')->identifier('phpunit.assertTrue')->build(), diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 83807ec9..3362f443 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -23,21 +23,23 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { + if (!$node instanceof Node\Expr\MethodCall && ! $node instanceof Node\Expr\StaticCall) { return []; } - - if ($node->isFirstClassCallable()) { + if (count($node->getArgs()) < 2) { return []; } - - if (count($node->getArgs()) < 2) { + if ($node->isFirstClassCallable()) { return []; } if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { return []; } + if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { + return []; + } + $expectedArgumentValue = $node->getArgs()[0]->value; if (!($expectedArgumentValue instanceof ConstFetch)) { return []; diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 9e051cc8..0e6d83cf 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -24,23 +24,24 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { + if (!$node instanceof Node\Expr\MethodCall && ! $node instanceof Node\Expr\StaticCall) { + return []; + } + if (count($node->getArgs()) < 2) { return []; } - if ($node->isFirstClassCallable()) { return []; } - - if (count($node->getArgs()) < 2) { + if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { return []; } - if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { + + if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) { return []; } $right = $node->getArgs()[1]->value; - if ( $right instanceof Node\Expr\FuncCall && $right->name instanceof Node\Name From 758d343c748bf09c834f03a89a3e5168ba250e50 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Oct 2025 07:28:51 +0100 Subject: [PATCH 264/277] Refactor PHPUnitVersionDetector to ease different major version checks --- extension.neon | 8 +- src/Rules/PHPUnit/DataProviderHelper.php | 14 +- .../PHPUnit/DataProviderHelperFactory.php | 8 +- src/Rules/PHPUnit/PHPUnitVersion.php | 41 ++++++ src/Rules/PHPUnit/PHPUnitVersionDetector.php | 15 +-- src/Rules/PHPUnit/TestMethodsHelper.php | 8 +- .../PHPUnit/TestMethodsHelperFactory.php | 28 ---- .../PHPUnit/DataProviderDataRuleTest.php | 13 +- .../DataProviderDeclarationRuleTest.php | 123 ++++++++++++------ 9 files changed, 159 insertions(+), 99 deletions(-) create mode 100644 src/Rules/PHPUnit/PHPUnitVersion.php delete mode 100644 src/Rules/PHPUnit/TestMethodsHelperFactory.php diff --git a/extension.neon b/extension.neon index dcf43818..23ae52cf 100644 --- a/extension.neon +++ b/extension.neon @@ -53,13 +53,13 @@ services: class: PHPStan\Rules\PHPUnit\AnnotationHelper - - class: PHPStan\Rules\PHPUnit\PHPUnitVersionDetector + class: PHPStan\Rules\PHPUnit\TestMethodsHelper - - class: PHPStan\Rules\PHPUnit\TestMethodsHelper - factory: @PHPStan\Rules\PHPUnit\TestMethodsHelperFactory::create() + class: PHPStan\Rules\PHPUnit\PHPUnitVersion + factory: @PHPStan\Rules\PHPUnit\PHPUnitVersionDetector::createPHPUnitVersion() - - class: PHPStan\Rules\PHPUnit\TestMethodsHelperFactory + class: PHPStan\Rules\PHPUnit\PHPUnitVersionDetector - class: PHPStan\Rules\PHPUnit\DataProviderHelper diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index b468dfcb..d40d05e4 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -37,19 +37,19 @@ class DataProviderHelper private Parser $parser; - private bool $phpunit10OrNewer; + private PHPUnitVersion $PHPUnitVersion; public function __construct( ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper, Parser $parser, - bool $phpunit10OrNewer + PHPUnitVersion $PHPUnitVersion ) { $this->reflectionProvider = $reflectionProvider; $this->fileTypeMapper = $fileTypeMapper; $this->parser = $parser; - $this->phpunit10OrNewer = $phpunit10OrNewer; + $this->PHPUnitVersion = $PHPUnitVersion; } /** @@ -65,7 +65,7 @@ public function getDataProviderMethods( { yield from $this->yieldDataProviderAnnotations($testMethod, $scope, $classReflection); - if (!$this->phpunit10OrNewer) { + if (!$this->PHPUnitVersion->supportsDataProviderAttribute()->yes()) { return; } @@ -156,7 +156,11 @@ public function processDataProvider( ->build(); } - if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) { + if ( + $deprecationRulesInstalled + && $this->PHPUnitVersion->requiresStaticDataProviders()->yes() + && !$dataProviderMethodReflection->isStatic() + ) { $errorBuilder = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be static in PHPUnit 10 and newer.', $dataProviderValue, diff --git a/src/Rules/PHPUnit/DataProviderHelperFactory.php b/src/Rules/PHPUnit/DataProviderHelperFactory.php index 23a5f34a..33bbe22d 100644 --- a/src/Rules/PHPUnit/DataProviderHelperFactory.php +++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php @@ -15,24 +15,24 @@ class DataProviderHelperFactory private Parser $parser; - private PHPUnitVersionDetector $PHPUnitVersionDetector; + private PHPUnitVersion $PHPUnitVersion; public function __construct( ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper, Parser $parser, - PHPUnitVersionDetector $PHPUnitVersionDetector + PHPUnitVersion $PHPUnitVersion ) { $this->reflectionProvider = $reflectionProvider; $this->fileTypeMapper = $fileTypeMapper; $this->parser = $parser; - $this->PHPUnitVersionDetector = $PHPUnitVersionDetector; + $this->PHPUnitVersion = $PHPUnitVersion; } public function create(): DataProviderHelper { - return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $this->parser, $this->PHPUnitVersionDetector->isPHPUnit10OrNewer()); + return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $this->parser, $this->PHPUnitVersion); } } diff --git a/src/Rules/PHPUnit/PHPUnitVersion.php b/src/Rules/PHPUnit/PHPUnitVersion.php new file mode 100644 index 00000000..932deedd --- /dev/null +++ b/src/Rules/PHPUnit/PHPUnitVersion.php @@ -0,0 +1,41 @@ +majorVersion = $majorVersion; + } + + public function supportsDataProviderAttribute(): TrinaryLogic + { + if ($this->majorVersion === null) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createFromBoolean($this->majorVersion >= 10); + } + + public function supportsTestAttribute(): TrinaryLogic + { + if ($this->majorVersion === null) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createFromBoolean($this->majorVersion >= 10); + } + + public function requiresStaticDataProviders(): TrinaryLogic + { + if ($this->majorVersion === null) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createFromBoolean($this->majorVersion >= 10); + } + +} diff --git a/src/Rules/PHPUnit/PHPUnitVersionDetector.php b/src/Rules/PHPUnit/PHPUnitVersionDetector.php index 841e69b9..f0e2c4b9 100644 --- a/src/Rules/PHPUnit/PHPUnitVersionDetector.php +++ b/src/Rules/PHPUnit/PHPUnitVersionDetector.php @@ -13,8 +13,6 @@ class PHPUnitVersionDetector { - private ?bool $is10OrNewer = null; - private ReflectionProvider $reflectionProvider; public function __construct(ReflectionProvider $reflectionProvider) @@ -22,13 +20,9 @@ public function __construct(ReflectionProvider $reflectionProvider) $this->reflectionProvider = $reflectionProvider; } - public function isPHPUnit10OrNewer(): bool + public function createPHPUnitVersion(): PHPUnitVersion { - if ($this->is10OrNewer !== null) { - return $this->is10OrNewer; - } - - $this->is10OrNewer = false; + $majorVersion = null; if ($this->reflectionProvider->hasClass(TestCase::class)) { $testCase = $this->reflectionProvider->getClass(TestCase::class); $file = $testCase->getFileName(); @@ -42,16 +36,13 @@ public function isPHPUnit10OrNewer(): bool $version = $json['extra']['branch-alias']['dev-main'] ?? null; if ($version !== null) { $majorVersion = (int) explode('.', $version)[0]; - if ($majorVersion >= 10) { - $this->is10OrNewer = true; - } } } } } } - return $this->is10OrNewer; + return new PHPUnitVersion($majorVersion); } } diff --git a/src/Rules/PHPUnit/TestMethodsHelper.php b/src/Rules/PHPUnit/TestMethodsHelper.php index 67b888e7..5eb274e0 100644 --- a/src/Rules/PHPUnit/TestMethodsHelper.php +++ b/src/Rules/PHPUnit/TestMethodsHelper.php @@ -16,15 +16,15 @@ final class TestMethodsHelper private FileTypeMapper $fileTypeMapper; - private bool $phpunit10OrNewer; + private PHPUnitVersion $PHPUnitVersion; public function __construct( FileTypeMapper $fileTypeMapper, - bool $phpunit10OrNewer + PHPUnitVersion $PHPUnitVersion ) { $this->fileTypeMapper = $fileTypeMapper; - $this->phpunit10OrNewer = $phpunit10OrNewer; + $this->PHPUnitVersion = $PHPUnitVersion; } /** @@ -63,7 +63,7 @@ public function getTestMethods(ClassReflection $classReflection, Scope $scope): } } - if (!$this->phpunit10OrNewer) { + if ($this->PHPUnitVersion->supportsTestAttribute()->no()) { continue; } diff --git a/src/Rules/PHPUnit/TestMethodsHelperFactory.php b/src/Rules/PHPUnit/TestMethodsHelperFactory.php deleted file mode 100644 index 202f88cb..00000000 --- a/src/Rules/PHPUnit/TestMethodsHelperFactory.php +++ /dev/null @@ -1,28 +0,0 @@ -fileTypeMapper = $fileTypeMapper; - $this->PHPUnitVersionDetector = $PHPUnitVersionDetector; - } - - public function create(): TestMethodsHelper - { - return new TestMethodsHelper($this->fileTypeMapper, $this->PHPUnitVersionDetector->isPHPUnit10OrNewer()); - } - -} diff --git a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php index 647f830f..e3f80a3b 100644 --- a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php @@ -14,6 +14,7 @@ */ class DataProviderDataRuleTest extends RuleTestCase { + private int $phpunitVersion; protected function getRule(): Rule { @@ -24,13 +25,13 @@ protected function getRule(): Rule new DataProviderDataRule( new TestMethodsHelper( self::getContainer()->getByType(FileTypeMapper::class), - true + new PHPUnitVersion($this->phpunitVersion) ), new DataProviderHelper( $reflectionProvider, self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getService('defaultAnalysisParser'), - true + new PHPUnitVersion($this->phpunitVersion) ), ), @@ -42,6 +43,8 @@ protected function getRule(): Rule public function testRule(): void { + $this->phpunitVersion = 10; + $this->analyse([__DIR__ . '/data/data-provider-data.php'], [ [ 'Parameter #2 $input of method DataProviderDataTest\FooTest::testWithAttribute() expects string, int given.', @@ -176,6 +179,8 @@ public function testRulePhp8(): void self::markTestSkipped(); } + $this->phpunitVersion = 10; + $this->analyse([__DIR__ . '/data/data-provider-data-named.php'], [ [ 'Parameter $input of method DataProviderDataTestPhp8\NamedArgsInProvider::testFoo() expects string, int given.', @@ -203,6 +208,8 @@ public function testRulePhp8(): void public function testVariadicMethod(): void { + $this->phpunitVersion = 10; + $this->analyse([__DIR__ . '/data/data-provider-variadic-method.php'], [ [ 'Method DataProviderVariadicMethod\FooTest::testProvide2() invoked with 1 parameter, at least 2 required.', @@ -241,6 +248,8 @@ public function testVariadicMethod(): void public function testTrimmingArgs(): void { + $this->phpunitVersion = 10; + $this->analyse([__DIR__ . '/data/data-provider-trimming-args.php'], [ [ 'Method DataProviderTrimmingArgs\FooTest::testProvide() invoked with 2 parameters, 1 required.', diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index 67f55ed4..2bf9d870 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -5,12 +5,15 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use PHPUnit\Framework\Attributes\DataProvider; + /** * @extends RuleTestCase */ class DataProviderDeclarationRuleTest extends RuleTestCase { + private ?int $phpunitVersion; protected function getRule(): Rule { @@ -21,57 +24,97 @@ protected function getRule(): Rule $reflection, self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getService('defaultAnalysisParser'), - true + new PHPUnitVersion($this->phpunitVersion) ), true, true ); } - public function testRule(): void + /** + * @dataProvider provideVersions + */ + #[DataProvider('provideVersions')] + public function testRule(?int $version): void { - $this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [ - [ - '@dataProvider providebaz related method is used with incorrect case: provideBaz.', - 16, - ], - [ - '@dataProvider provideQux related method must be static in PHPUnit 10 and newer.', - 16, - ], - [ - '@dataProvider provideQuux related method must be public.', - 16, - ], - [ - '@dataProvider provideNonExisting related method not found.', - 70, - ], - [ - '@dataProvider NonExisting::provideNonExisting related class not found.', - 70, - ], - [ - '@dataProvider provideNonExisting related method not found.', - 85, - ], - [ - '@dataProvider provideNonExisting2 related method not found.', - 86, - ], - [ - '@dataProvider ExampleTestCase\\BarTestCase::providetootherclass related method is used with incorrect case: provideToOtherClass.', - 87, - ], - [ - '@dataProvider ExampleTestCase\\BarTestCase::providetootherclass related method is used with incorrect case: provideToOtherClass.', - 88, - ], - ]); + $this->phpunitVersion = $version; + + if ($version >= 10) { + $errors = [ + [ + '@dataProvider providebaz related method is used with incorrect case: provideBaz.', + 16, + ], + [ + '@dataProvider provideQux related method must be static in PHPUnit 10 and newer.', + 16, + ], + [ + '@dataProvider provideQuux related method must be public.', + 16, + ], + [ + '@dataProvider provideNonExisting related method not found.', + 70, + ], + [ + '@dataProvider NonExisting::provideNonExisting related class not found.', + 70, + ], + [ + '@dataProvider provideNonExisting related method not found.', + 85, + ], + [ + '@dataProvider provideNonExisting2 related method not found.', + 86, + ], + [ + '@dataProvider ExampleTestCase\\BarTestCase::providetootherclass related method is used with incorrect case: provideToOtherClass.', + 87, + ], + [ + '@dataProvider ExampleTestCase\\BarTestCase::providetootherclass related method is used with incorrect case: provideToOtherClass.', + 88, + ], + ]; + } else { + $errors = [ + [ + '@dataProvider providebaz related method is used with incorrect case: provideBaz.', + 16, + ], + [ + '@dataProvider provideQuux related method must be public.', + 16, + ], + [ + '@dataProvider provideNonExisting related method not found.', + 70, + ], + [ + '@dataProvider NonExisting::provideNonExisting related class not found.', + 70, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/data-provider-declaration.php'], $errors); + } + + static public function provideVersions(): iterable + { + return [ + [null], + [9], + [10] + ]; } public function testFixDataProviderStatic(): void { + $this->phpunitVersion = 10; + $this->fix(__DIR__ . '/data/data-provider-static-fix.php', __DIR__ . '/data/data-provider-static-fix.php.fixed'); } From 033f6906f7abc5acbe3c81b642ad9e363e1bb4a0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Oct 2025 09:37:09 +0100 Subject: [PATCH 265/277] PHPUnit 9.x/10.x does not at all support named arguments from data-providers --- src/Rules/PHPUnit/DataProviderDataRule.php | 9 +- src/Rules/PHPUnit/PHPUnitVersion.php | 8 ++ .../PHPUnit/DataProviderDataRuleTest.php | 126 ++++++++++++++---- .../PHPUnit/data/data-provider-named-args.php | 32 +++++ 4 files changed, 144 insertions(+), 31 deletions(-) create mode 100644 tests/Rules/PHPUnit/data/data-provider-named-args.php diff --git a/src/Rules/PHPUnit/DataProviderDataRule.php b/src/Rules/PHPUnit/DataProviderDataRule.php index 6f250fee..ce994676 100644 --- a/src/Rules/PHPUnit/DataProviderDataRule.php +++ b/src/Rules/PHPUnit/DataProviderDataRule.php @@ -23,13 +23,17 @@ class DataProviderDataRule implements Rule private DataProviderHelper $dataProviderHelper; + private PHPUnitVersion $PHPUnitVersion; + public function __construct( TestMethodsHelper $testMethodsHelper, - DataProviderHelper $dataProviderHelper + DataProviderHelper $dataProviderHelper, + PHPUnitVersion $PHPUnitVersion ) { $this->testMethodsHelper = $testMethodsHelper; $this->dataProviderHelper = $dataProviderHelper; + $this->PHPUnitVersion = $PHPUnitVersion; } public function getNodeType(): string @@ -153,11 +157,10 @@ private function arrayItemsToArgs(Type $array, int $numberOfParameters): ?array return null; } - if (count($key) === 0) { + if (count($key) === 0 || !$this->PHPUnitVersion->supportsNamedArgumentsInDataProvider()->yes()) { $arg = new Node\Arg(new TypeExpr($valueType)); $args[] = $arg; continue; - } $arg = new Node\Arg( diff --git a/src/Rules/PHPUnit/PHPUnitVersion.php b/src/Rules/PHPUnit/PHPUnitVersion.php index 932deedd..b7259a84 100644 --- a/src/Rules/PHPUnit/PHPUnitVersion.php +++ b/src/Rules/PHPUnit/PHPUnitVersion.php @@ -38,4 +38,12 @@ public function requiresStaticDataProviders(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->majorVersion >= 10); } + public function supportsNamedArgumentsInDataProvider(): TrinaryLogic + { + if ($this->majorVersion === null) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createFromBoolean($this->majorVersion >= 11); + } + } diff --git a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php index e3f80a3b..cca88e7f 100644 --- a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php @@ -8,32 +8,36 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\TestWith; +use const PHP_VERSION_ID; /** * @extends RuleTestCase */ class DataProviderDataRuleTest extends RuleTestCase { - private int $phpunitVersion; + private ?int $phpunitVersion; protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); + $phpunitVersion = new PHPUnitVersion($this->phpunitVersion); /** @var list> $rules */ $rules = [ new DataProviderDataRule( new TestMethodsHelper( self::getContainer()->getByType(FileTypeMapper::class), - new PHPUnitVersion($this->phpunitVersion) + $phpunitVersion ), new DataProviderHelper( $reflectionProvider, self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getService('defaultAnalysisParser'), - new PHPUnitVersion($this->phpunitVersion) + $phpunitVersion ), - + $phpunitVersion, ), self::getContainer()->getByType(CallMethodsRule::class) /** @phpstan-ignore phpstanApi.classConstant */ ]; @@ -173,36 +177,64 @@ public function testRule(): void ]); } - public function testRulePhp8(): void + + /** + * @dataProvider provideNamedArgumentPHPUnitVersions + */ + #[DataProvider('provideNamedArgumentPHPUnitVersions')] + public function testRulePhp8(?int $phpunitVersion): void { if (PHP_VERSION_ID < 80000) { self::markTestSkipped(); } - $this->phpunitVersion = 10; + $this->phpunitVersion = $phpunitVersion; - $this->analyse([__DIR__ . '/data/data-provider-data-named.php'], [ - [ - 'Parameter $input of method DataProviderDataTestPhp8\NamedArgsInProvider::testFoo() expects string, int given.', - 44 - ], - [ - 'Parameter $input of method DataProviderDataTestPhp8\NamedArgsInProvider::testFoo() expects string, false given.', - 44 - ], - [ - 'Unknown parameter $wrong in call to method DataProviderDataTestPhp8\TestWrongOffsetNameArrayShapeIterable::testBar().', - 58 - ], - [ - 'Missing parameter $si (int) in call to method DataProviderDataTestPhp8\TestWrongOffsetNameArrayShapeIterable::testBar().', - 58 - ], - [ - 'Parameter $si of method DataProviderDataTestPhp8\TestWrongTypeInArrayShapeIterable::testBar() expects int, string given.', - 79 - ], - ]); + if ($phpunitVersion >= 11) { + $errors = [ + [ + 'Parameter $input of method DataProviderDataTestPhp8\NamedArgsInProvider::testFoo() expects string, int given.', + 44 + ], + [ + 'Parameter $input of method DataProviderDataTestPhp8\NamedArgsInProvider::testFoo() expects string, false given.', + 44 + ], + [ + 'Unknown parameter $wrong in call to method DataProviderDataTestPhp8\TestWrongOffsetNameArrayShapeIterable::testBar().', + 58 + ], + [ + 'Missing parameter $si (int) in call to method DataProviderDataTestPhp8\TestWrongOffsetNameArrayShapeIterable::testBar().', + 58 + ], + [ + 'Parameter $si of method DataProviderDataTestPhp8\TestWrongTypeInArrayShapeIterable::testBar() expects int, string given.', + 79 + ], + ]; + } else { + $errors = [ + [ + 'Parameter #1 $expectedResult of method DataProviderDataTestPhp8\NamedArgsInProvider::testFoo() expects string, int given.', + 44 + ], + [ + 'Parameter #1 $expectedResult of method DataProviderDataTestPhp8\NamedArgsInProvider::testFoo() expects string, false given.', + 44 + ], + [ + 'Parameter #1 $si of method DataProviderDataTestPhp8\TestWrongOffsetNameArrayShapeIterable::testBar() expects int, string given.', + 58 + ], + [ + 'Parameter #1 $si of method DataProviderDataTestPhp8\TestWrongTypeInArrayShapeIterable::testBar() expects int, string given.', + 79 + ], + ]; + } + + $this->analyse([__DIR__ . '/data/data-provider-data-named.php'], $errors); } @@ -274,6 +306,44 @@ public function testTrimmingArgs(): void ]); } + static public function provideNamedArgumentPHPUnitVersions(): iterable + { + yield [null]; // unknown phpunit version + + if (PHP_VERSION_ID >= 80100) { + yield [10]; // PHPUnit 10.x requires PHP 8.1+ + } + if (PHP_VERSION_ID >= 80200) { + yield [11]; // PHPUnit 11.x requires PHP 8.2+ + } + } + + /** + * @dataProvider provideNamedArgumentPHPUnitVersions + */ + #[DataProvider('provideNamedArgumentPHPUnitVersions')] + public function testNamedArgumentsInDataProviders(?int $phpunitVersion): void + { + $this->phpunitVersion = $phpunitVersion; + + if ($phpunitVersion >= 11) { + $errors = []; + } else { + $errors = [ + [ + 'Parameter #1 $int of method DataProviderNamedArgs\FooTest::testFoo() expects int, string given.', + 26 + ], + [ + 'Parameter #2 $string of method DataProviderNamedArgs\FooTest::testFoo() expects string, int given.', + 26 + ], + ]; + } + + $this->analyse([__DIR__ . '/data/data-provider-named-args.php'], $errors); + } + /** * @return string[] */ diff --git a/tests/Rules/PHPUnit/data/data-provider-named-args.php b/tests/Rules/PHPUnit/data/data-provider-named-args.php new file mode 100644 index 00000000..094ea3b1 --- /dev/null +++ b/tests/Rules/PHPUnit/data/data-provider-named-args.php @@ -0,0 +1,32 @@ +assertTrue(true); + } + + public static function dataProvider(): iterable + { + yield 'even' => [ + 'int' => 50, + 'string' => 'abc', + ]; + + yield 'odd' => [ + 'string' => 'def', + 'int' => 51, + ]; + } +} + From 450bfc08e2e5d00b214110de14168d4ebd210cfb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 24 Oct 2025 09:12:33 +0200 Subject: [PATCH 266/277] Added assertArrayHasKey() expectations --- tests/Type/PHPUnit/data/assert-function.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 0fabf59e..f7a27d94 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -11,7 +11,6 @@ use function PHPUnit\Framework\assertNotCount; use function PHPUnit\Framework\assertEmpty; use function PHPUnit\Framework\assertInstanceOf; -use function PHPUnit\Framework\assertObjectHasAttribute; class Foo { @@ -53,6 +52,12 @@ public function arrayHasStringKey(array $a, \ArrayAccess $b): void assertType("ArrayAccess", $b); } + public function arrayHasExprKey(int $index, array $a): void + { + assertArrayHasKey($index, $a); + assertType("non-empty-array", $a); + } + public function testEmpty($a): void { assertEmpty($a); From 36cb9a621f7e7c3ee667dd7f60be6614a2f1664b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 2 Nov 2025 09:23:04 +0100 Subject: [PATCH 267/277] Make AssertSameBooleanExpectedRule auto-fixable --- .../PHPUnit/AssertEqualsIsDiscouragedRule.php | 8 +---- .../PHPUnit/AssertSameBooleanExpectedRule.php | 33 +++++++++++++++++-- .../AssertSameBooleanExpectedRuleTest.php | 5 +++ .../assert-same-boolean-expected-fixable.php | 21 ++++++++++++ ...rt-same-boolean-expected-fixable.php.fixed | 21 ++++++++++++ 5 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 tests/Rules/PHPUnit/data/assert-same-boolean-expected-fixable.php create mode 100644 tests/Rules/PHPUnit/data/assert-same-boolean-expected-fixable.php.fixed diff --git a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php index c4c23887..ed6470e2 100644 --- a/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php +++ b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php @@ -73,13 +73,7 @@ public function processNode(Node $node, Scope $scope): array ), )->identifier('phpunit.assertEquals') ->fixNode($node, static function (CallLike $node) use ($correctName) { - if ($node instanceof Node\Expr\MethodCall) { - $node->name = new Node\Identifier($correctName); - } - - if ($node instanceof Node\Expr\StaticCall) { - $node->name = new Node\Identifier($correctName); - } + $node->name = new Node\Identifier($correctName); return $node; }) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index fd76f7c4..f185bdf7 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -47,17 +47,46 @@ public function processNode(Node $node, Scope $scope): array if ($expectedArgumentValue->name->toLowerString() === 'true') { return [ - RuleErrorBuilder::message('You should use assertTrue() instead of assertSame() when expecting "true"')->identifier('phpunit.assertTrue')->build(), + RuleErrorBuilder::message('You should use assertTrue() instead of assertSame() when expecting "true"') + ->identifier('phpunit.assertTrue') + ->fixNode($node, static function (CallLike $node) { + $node->name = new Node\Identifier('assertTrue'); + $node->args = self::rewriteArgs($node->args); + + return $node; + }) + ->build(), ]; } if ($expectedArgumentValue->name->toLowerString() === 'false') { return [ - RuleErrorBuilder::message('You should use assertFalse() instead of assertSame() when expecting "false"')->identifier('phpunit.assertFalse')->build(), + RuleErrorBuilder::message('You should use assertFalse() instead of assertSame() when expecting "false"') + ->identifier('phpunit.assertFalse') + ->fixNode($node, static function (CallLike $node) { + $node->name = new Node\Identifier('assertFalse'); + $node->args = self::rewriteArgs($node->args); + + return $node; + }) + ->build(), ]; } return []; } + /** + * @param array $args + * @return list + */ + private static function rewriteArgs(array $args): array + { + $newArgs = []; + for ($i = 1; $i < count($args); $i++) { + $newArgs[] = $args[$i]; + } + return $newArgs; + } + } diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php index 1fe31df9..6dacd685 100644 --- a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -42,6 +42,11 @@ public function testRule(): void ]); } + public function testFix(): void + { + $this->fix(__DIR__ . '/data/assert-same-boolean-expected-fixable.php', __DIR__ . '/data/assert-same-boolean-expected-fixable.php.fixed'); + } + /** * @return string[] */ diff --git a/tests/Rules/PHPUnit/data/assert-same-boolean-expected-fixable.php b/tests/Rules/PHPUnit/data/assert-same-boolean-expected-fixable.php new file mode 100644 index 00000000..5d5a3ba4 --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-same-boolean-expected-fixable.php @@ -0,0 +1,21 @@ +assertSame(true, $this->returnBool()); + self::assertSame(false, $this->returnBool()); + } + +} diff --git a/tests/Rules/PHPUnit/data/assert-same-boolean-expected-fixable.php.fixed b/tests/Rules/PHPUnit/data/assert-same-boolean-expected-fixable.php.fixed new file mode 100644 index 00000000..d0bb802a --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-same-boolean-expected-fixable.php.fixed @@ -0,0 +1,21 @@ +assertTrue($this->returnBool()); + self::assertFalse($this->returnBool()); + } + +} From a4abfc11b1486ad1aff02661c3fd3982e1f904d7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 4 Nov 2025 17:05:44 +0100 Subject: [PATCH 268/277] Use TypeSystem in AssertSameBooleanExpectedRule --- .../data/assert-same-boolean-expected.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php index dccd2ceb..d6f7f14a 100644 --- a/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php +++ b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php @@ -75,6 +75,26 @@ public function testNonLowercase(): void \PHPUnit\Framework\Assert::assertSame(False, 'foo'); } + public function testMaybeTrueFalse(): void + { + $a = rand(0, 1) ? true : 'foo'; + \PHPUnit\Framework\Assert::assertSame($a, 'foo'); + $a = rand(0, 1) ? false : 'foo'; + \PHPUnit\Framework\Assert::assertSame($a, 'foo'); + } + + public function testConstMaybeTrueFalse(): void + { + if ( + !defined('MY_TEST_CONST') + ) { + return; + } + if (MY_TEST_CONST !== true && MY_TEST_CONST !== false) { + return; + } + \PHPUnit\Framework\Assert::assertSame(MY_TEST_CONST, 'foo'); + } } const PHPSTAN_PHPUNIT_TRUE = true; From 8532ceb49e02884cbd8ae7da63d62224c8eb44f5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 3 Nov 2025 15:01:09 +0100 Subject: [PATCH 269/277] Make AssertSameNullExpectedRule auto-fixable --- .../PHPUnit/AssertSameNullExpectedRule.php | 23 ++++++++++++++++++- .../AssertSameNullExpectedRuleTest.php | 5 ++++ .../assert-same-null-expected-fixable.php | 22 ++++++++++++++++++ ...ssert-same-null-expected-fixable.php.fixed | 22 ++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/Rules/PHPUnit/data/assert-same-null-expected-fixable.php create mode 100644 tests/Rules/PHPUnit/data/assert-same-null-expected-fixable.php.fixed diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 3362f443..f6032efb 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -47,11 +47,32 @@ public function processNode(Node $node, Scope $scope): array if ($expectedArgumentValue->name->toLowerString() === 'null') { return [ - RuleErrorBuilder::message('You should use assertNull() instead of assertSame(null, $actual).')->identifier('phpunit.assertNull')->build(), + RuleErrorBuilder::message('You should use assertNull() instead of assertSame(null, $actual).') + ->identifier('phpunit.assertNull') + ->fixNode($node, static function (CallLike $node) { + $node->name = new Node\Identifier('assertNull'); + $node->args = self::rewriteArgs($node->args); + + return $node; + }) + ->build(), ]; } return []; } + /** + * @param array $args + * @return list + */ + private static function rewriteArgs(array $args): array + { + $newArgs = []; + for ($i = 1; $i < count($args); $i++) { + $newArgs[] = $args[$i]; + } + return $newArgs; + } + } diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php index 1e802dc0..e29096d9 100644 --- a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -34,6 +34,11 @@ public function testRule(): void ]); } + public function testFix(): void + { + $this->fix(__DIR__ . '/data/assert-same-null-expected-fixable.php', __DIR__ . '/data/assert-same-null-expected-fixable.php.fixed'); + } + /** * @return string[] */ diff --git a/tests/Rules/PHPUnit/data/assert-same-null-expected-fixable.php b/tests/Rules/PHPUnit/data/assert-same-null-expected-fixable.php new file mode 100644 index 00000000..f95fadd6 --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-same-null-expected-fixable.php @@ -0,0 +1,22 @@ +assertSame(null, 'a'); + + \PHPUnit\Framework\Assert::assertSame($this->returnNull(), 'foo'); + } + +} diff --git a/tests/Rules/PHPUnit/data/assert-same-null-expected-fixable.php.fixed b/tests/Rules/PHPUnit/data/assert-same-null-expected-fixable.php.fixed new file mode 100644 index 00000000..a3c91042 --- /dev/null +++ b/tests/Rules/PHPUnit/data/assert-same-null-expected-fixable.php.fixed @@ -0,0 +1,22 @@ +assertNull('a'); + + \PHPUnit\Framework\Assert::assertSame($this->returnNull(), 'foo'); + } + +} From d935297c98d40c2756fc44e571010a6196239ea7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 11 Nov 2025 07:54:22 +0100 Subject: [PATCH 270/277] Fix "Unexpected input(s) 'extensions', valid inputs are ['php-version', 'php-extensions', 'build-infection-path']" --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 50334926..9299e527 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -246,7 +246,7 @@ jobs: - uses: ./build-infection/.github/actions/setup-php with: php-version: "${{ matrix.php-version }}" - extensions: mbstring + php-extensions: mbstring - name: "Install dependencies" run: "composer install --no-interaction --no-progress" From 2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 11 Nov 2025 07:26:10 +0100 Subject: [PATCH 271/277] Fix AssertSameWithCountRule for recursive count() --- src/Rules/PHPUnit/AssertSameWithCountRule.php | 67 ++++++++++++++----- .../PHPUnit/AssertSameWithCountRuleTest.php | 8 +++ .../Rules/PHPUnit/data/assert-same-count.php | 26 ++++++- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 0e6d83cf..2a5a7651 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -8,8 +8,12 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use function count; +use const COUNT_NORMAL; /** * @implements Rule @@ -42,11 +46,7 @@ public function processNode(Node $node, Scope $scope): array } $right = $node->getArgs()[1]->value; - if ( - $right instanceof Node\Expr\FuncCall - && $right->name instanceof Node\Name - && $right->name->toLowerString() === 'count' - ) { + if (self::isCountFunctionCall($right, $scope)) { return [ RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).') ->identifier('phpunit.assertCount') @@ -54,24 +54,59 @@ public function processNode(Node $node, Scope $scope): array ]; } + if (self::isCountableMethodCall($right, $scope)) { + return [ + RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).') + ->identifier('phpunit.assertCount') + ->build(), + ]; + } + + return []; + } + + /** + * @phpstan-assert-if-true Node\Expr\FuncCall $expr + */ + private static function isCountFunctionCall(Node\Expr $expr, Scope $scope): bool + { + return $expr instanceof Node\Expr\FuncCall + && $expr->name instanceof Node\Name + && $expr->name->toLowerString() === 'count' + && count($expr->getArgs()) >= 1 + && self::isNormalCount($expr, $scope->getType($expr->getArgs()[0]->value), $scope)->yes(); + } + + /** + * @phpstan-assert-if-true Node\Expr\MethodCall $expr + */ + private static function isCountableMethodCall(Node\Expr $expr, Scope $scope): bool + { if ( - $right instanceof Node\Expr\MethodCall - && $right->name instanceof Node\Identifier - && $right->name->toLowerString() === 'count' - && count($right->getArgs()) === 0 + $expr instanceof Node\Expr\MethodCall + && $expr->name instanceof Node\Identifier + && $expr->name->toLowerString() === 'count' + && count($expr->getArgs()) === 0 ) { - $type = $scope->getType($right->var); + $type = $scope->getType($expr->var); if ((new ObjectType(Countable::class))->isSuperTypeOf($type)->yes()) { - return [ - RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).') - ->identifier('phpunit.assertCount') - ->build(), - ]; + return true; } } - return []; + return false; + } + + private static function isNormalCount(Node\Expr\FuncCall $countFuncCall, Type $countedType, Scope $scope): TrinaryLogic + { + if (count($countFuncCall->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($countedType->getIterableValueType()->isArray()->negate()); + } + return $isNormalCount; } } diff --git a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php index 32f564d6..dfb940cf 100644 --- a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php @@ -31,6 +31,14 @@ public function testRule(): void 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).', 30, ], + [ + 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', + 40, + ], + [ + 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', + 45, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/assert-same-count.php b/tests/Rules/PHPUnit/data/assert-same-count.php index 73df333d..bc40eed7 100644 --- a/tests/Rules/PHPUnit/data/assert-same-count.php +++ b/tests/Rules/PHPUnit/data/assert-same-count.php @@ -30,6 +30,30 @@ public function testAssertSameWithCountMethodForCountableVariableIsNotOK() $this->assertSame(5, $foo->bar->count()); } + public function testRecursiveCount($x) + { + $this->assertSame(5, count([1, 2, 3, $x], COUNT_RECURSIVE)); // OK + } + + public function testNormalCount($x) + { + $this->assertSame(5, count([1, 2, 3, $x], COUNT_NORMAL)); + } + + public function testImplicitNormalCount($mode) + { + $this->assertSame(5, count([1, 2, 3], $mode)); + } + + public function testUnknownCountable($x, $mode) + { + $this->assertSame(5, count($x, $mode)); // OK + } + + public function testUnknownCountMode($x, $mode) + { + $this->assertSame(5, count([1, 2, 3, $x], $mode)); // OK + } } class Bar implements \Countable { @@ -37,4 +61,4 @@ public function count(): int { return 1; } -}; +} From 75ba9486ffc729a3b5885202a76dc13930890e2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 02:08:16 +0000 Subject: [PATCH 272/277] chore(deps): update actions/checkout action to v6 --- .github/workflows/build.yml | 14 +++++++------- .github/workflows/create-tag.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9299e527..979caa44 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -53,10 +53,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Checkout build-cs" - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: "phpstan/build-cs" path: "build-cs" @@ -128,7 +128,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -194,7 +194,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -234,10 +234,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Checkout build-infection" - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: "phpstan/build-infection" path: "build-infection" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index fd918164..a946f1c7 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -21,7 +21,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed7e51ad..efb9bbeb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Generate changelog id: changelog From 83b717d01e4dc0525d6707508d9af9dcbd4d8049 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 25 Nov 2025 22:17:31 +0100 Subject: [PATCH 273/277] ci: Add tests for PHP 8.5 --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 979caa44..b74773b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,7 @@ jobs: - "8.2" - "8.3" - "8.4" + - "8.5" steps: - name: "Checkout" @@ -98,6 +99,7 @@ jobs: - "8.2" - "8.3" - "8.4" + - "8.5" dependencies: - "lowest" - "highest" @@ -164,6 +166,7 @@ jobs: - "8.2" - "8.3" - "8.4" + - "8.5" dependencies: - "lowest" - "highest" @@ -230,6 +233,7 @@ jobs: - "8.2" - "8.3" - "8.4" + - "8.5" operating-system: [ubuntu-latest] steps: From cda423c709dae0126987fec04a381ec819c9d9a7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 5 Dec 2025 09:04:44 +0100 Subject: [PATCH 274/277] Implement AttributeRequiresPhpVersionRule --- rules.neon | 7 ++ .../AttributeRequiresPhpVersionRule.php | 99 +++++++++++++++++++ src/Rules/PHPUnit/PHPUnitVersion.php | 35 ++++++- src/Rules/PHPUnit/PHPUnitVersionDetector.php | 7 +- src/Rules/PHPUnit/TestMethodsHelper.php | 12 +++ .../AttributeRequiresPhpVersionRuleTest.php | 95 ++++++++++++++++++ .../PHPUnit/DataProviderDataRuleTest.php | 2 +- .../DataProviderDeclarationRuleTest.php | 2 +- .../PHPUnit/data/requires-php-version.php | 24 +++++ 9 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php create mode 100644 tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/requires-php-version.php diff --git a/rules.neon b/rules.neon index 8272f47a..8469bd1e 100644 --- a/rules.neon +++ b/rules.neon @@ -25,6 +25,13 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\PHPUnit\AttributeRequiresPhpVersionRule + arguments: + deprecationRulesInstalled: %deprecationRulesInstalled% + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\PHPUnit\AssertEqualsIsDiscouragedRule diff --git a/src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php b/src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php new file mode 100644 index 00000000..f106bf8a --- /dev/null +++ b/src/Rules/PHPUnit/AttributeRequiresPhpVersionRule.php @@ -0,0 +1,99 @@ + + */ +class AttributeRequiresPhpVersionRule implements Rule +{ + + private PHPUnitVersion $PHPUnitVersion; + + private TestMethodsHelper $testMethodsHelper; + + /** + * When phpstan-deprecation-rules is installed, it reports deprecated usages. + */ + private bool $deprecationRulesInstalled; + + public function __construct( + PHPUnitVersion $PHPUnitVersion, + TestMethodsHelper $testMethodsHelper, + bool $deprecationRulesInstalled + ) + { + $this->PHPUnitVersion = $PHPUnitVersion; + $this->testMethodsHelper = $testMethodsHelper; + $this->deprecationRulesInstalled = $deprecationRulesInstalled; + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + if ($classReflection === null || $classReflection->is(TestCase::class) === false) { + return []; + } + + $reflectionMethod = $this->testMethodsHelper->getTestMethodReflection($classReflection, $node->getMethodReflection(), $scope); + if ($reflectionMethod === null) { + return []; + } + + /** @phpstan-ignore function.alreadyNarrowedType */ + if (!method_exists($reflectionMethod, 'getAttributes')) { + return []; + } + + $errors = []; + foreach ($reflectionMethod->getAttributes('PHPUnit\Framework\Attributes\RequiresPhp') as $attr) { + $args = $attr->getArguments(); + if (count($args) !== 1) { + continue; + } + + if ( + !is_numeric($args[0]) + ) { + continue; + } + + if ($this->PHPUnitVersion->requiresPhpversionAttributeWithOperator()->yes()) { + $errors[] = RuleErrorBuilder::message( + sprintf('Version requirement is missing operator.'), + ) + ->identifier('phpunit.attributeRequiresPhpVersion') + ->build(); + } elseif ( + $this->deprecationRulesInstalled + && $this->PHPUnitVersion->deprecatesPhpversionAttributeWithoutOperator()->yes() + ) { + $errors[] = RuleErrorBuilder::message( + sprintf('Version requirement without operator is deprecated.'), + ) + ->identifier('phpunit.attributeRequiresPhpVersion') + ->build(); + } + + } + + return $errors; + } + +} diff --git a/src/Rules/PHPUnit/PHPUnitVersion.php b/src/Rules/PHPUnit/PHPUnitVersion.php index b7259a84..56eb7558 100644 --- a/src/Rules/PHPUnit/PHPUnitVersion.php +++ b/src/Rules/PHPUnit/PHPUnitVersion.php @@ -9,9 +9,12 @@ class PHPUnitVersion private ?int $majorVersion; - public function __construct(?int $majorVersion) + private ?int $minorVersion; + + public function __construct(?int $majorVersion, ?int $minorVersion) { $this->majorVersion = $majorVersion; + $this->minorVersion = $minorVersion; } public function supportsDataProviderAttribute(): TrinaryLogic @@ -46,4 +49,34 @@ public function supportsNamedArgumentsInDataProvider(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->majorVersion >= 11); } + public function requiresPhpversionAttributeWithOperator(): TrinaryLogic + { + if ($this->majorVersion === null) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createFromBoolean($this->majorVersion >= 13); + } + + public function deprecatesPhpversionAttributeWithoutOperator(): TrinaryLogic + { + return $this->minVersion(12, 4); + } + + private function minVersion(int $major, int $minor): TrinaryLogic + { + if ($this->majorVersion === null || $this->minorVersion === null) { + return TrinaryLogic::createMaybe(); + } + + if ($this->majorVersion > $major) { + return TrinaryLogic::createYes(); + } + + if ($this->majorVersion === $major && $this->minorVersion >= $minor) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createNo(); + } + } diff --git a/src/Rules/PHPUnit/PHPUnitVersionDetector.php b/src/Rules/PHPUnit/PHPUnitVersionDetector.php index f0e2c4b9..b82caaf4 100644 --- a/src/Rules/PHPUnit/PHPUnitVersionDetector.php +++ b/src/Rules/PHPUnit/PHPUnitVersionDetector.php @@ -23,6 +23,7 @@ public function __construct(ReflectionProvider $reflectionProvider) public function createPHPUnitVersion(): PHPUnitVersion { $majorVersion = null; + $minorVersion = null; if ($this->reflectionProvider->hasClass(TestCase::class)) { $testCase = $this->reflectionProvider->getClass(TestCase::class); $file = $testCase->getFileName(); @@ -35,14 +36,16 @@ public function createPHPUnitVersion(): PHPUnitVersion $json = json_decode($composerJson, true); $version = $json['extra']['branch-alias']['dev-main'] ?? null; if ($version !== null) { - $majorVersion = (int) explode('.', $version)[0]; + $versionParts = explode('.', $version); + $majorVersion = (int) $versionParts[0]; + $minorVersion = (int) $versionParts[1]; } } } } } - return new PHPUnitVersion($majorVersion); + return new PHPUnitVersion($majorVersion, $minorVersion); } } diff --git a/src/Rules/PHPUnit/TestMethodsHelper.php b/src/Rules/PHPUnit/TestMethodsHelper.php index 5eb274e0..a215c8f4 100644 --- a/src/Rules/PHPUnit/TestMethodsHelper.php +++ b/src/Rules/PHPUnit/TestMethodsHelper.php @@ -5,6 +5,7 @@ use PHPStan\Analyser\Scope; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\Type\FileTypeMapper; use PHPUnit\Framework\TestCase; use ReflectionMethod; @@ -27,6 +28,17 @@ public function __construct( $this->PHPUnitVersion = $PHPUnitVersion; } + public function getTestMethodReflection(ClassReflection $classReflection, MethodReflection $methodReflection, Scope $scope): ?ReflectionMethod + { + foreach ($this->getTestMethods($classReflection, $scope) as $testMethod) { + if ($testMethod->getName() === $methodReflection->getName()) { + return $testMethod; + } + } + + return null; + } + /** * @return array */ diff --git a/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php b/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php new file mode 100644 index 00000000..92d9715c --- /dev/null +++ b/tests/Rules/PHPUnit/AttributeRequiresPhpVersionRuleTest.php @@ -0,0 +1,95 @@ + + */ +final class AttributeRequiresPhpVersionRuleTest extends RuleTestCase +{ + + private ?int $phpunitMajorVersion; + + private ?int $phpunitMinorVersion; + + private bool $deprecationRulesInstalled = true; + + public function testRuleOnPHPUnitUnknown(): void + { + $this->phpunitMajorVersion = null; + $this->phpunitMinorVersion = null; + + $this->analyse([__DIR__ . '/data/requires-php-version.php'], []); + } + + public function testRuleOnPHPUnit115(): void + { + $this->phpunitMajorVersion = 11; + $this->phpunitMinorVersion = 5; + + $this->analyse([__DIR__ . '/data/requires-php-version.php'], []); + } + + public function testRuleOnPHPUnit123(): void + { + $this->phpunitMajorVersion = 12; + $this->phpunitMinorVersion = 3; + + $this->analyse([__DIR__ . '/data/requires-php-version.php'], []); + } + + public function testRuleOnPHPUnit124DeprecationsOn(): void + { + $this->phpunitMajorVersion = 12; + $this->phpunitMinorVersion = 4; + $this->deprecationRulesInstalled = true; + + $this->analyse([__DIR__ . '/data/requires-php-version.php'], [ + [ + 'Version requirement without operator is deprecated.', + 12, + ], + ]); + } + + public function testRuleOnPHPUnit124DeprecationsOff(): void + { + $this->phpunitMajorVersion = 12; + $this->phpunitMinorVersion = 4; + $this->deprecationRulesInstalled = false; + + $this->analyse([__DIR__ . '/data/requires-php-version.php'], []); + } + + public function testRuleOnPHPUnit13(): void + { + $this->phpunitMajorVersion = 13; + $this->phpunitMinorVersion = 0; + + $this->analyse([__DIR__ . '/data/requires-php-version.php'], [ + [ + 'Version requirement is missing operator.', + 12, + ], + ]); + } + + protected function getRule(): Rule + { + $phpunitVersion = new PHPUnitVersion($this->phpunitMajorVersion, $this->phpunitMinorVersion); + + return new AttributeRequiresPhpVersionRule( + $phpunitVersion, + new TestMethodsHelper( + self::getContainer()->getByType(FileTypeMapper::class), + $phpunitVersion, + ), + $this->deprecationRulesInstalled, + ); + } + +} diff --git a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php index cca88e7f..012fce70 100644 --- a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php @@ -22,7 +22,7 @@ class DataProviderDataRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $phpunitVersion = new PHPUnitVersion($this->phpunitVersion); + $phpunitVersion = new PHPUnitVersion($this->phpunitVersion, 0); /** @var list> $rules */ $rules = [ diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index 2bf9d870..f63c9e65 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -24,7 +24,7 @@ protected function getRule(): Rule $reflection, self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getService('defaultAnalysisParser'), - new PHPUnitVersion($this->phpunitVersion) + new PHPUnitVersion($this->phpunitVersion, 0) ), true, true diff --git a/tests/Rules/PHPUnit/data/requires-php-version.php b/tests/Rules/PHPUnit/data/requires-php-version.php new file mode 100644 index 00000000..5550edff --- /dev/null +++ b/tests/Rules/PHPUnit/data/requires-php-version.php @@ -0,0 +1,24 @@ +=8.0')] + public function testHappyPath(): void { + + } +} From 8d61a5854e7497d95bc85188e13537e99bd7aae7 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 6 Dec 2025 12:15:39 +0100 Subject: [PATCH 275/277] Remove checkDataProviderData and introduce reportMissingDataProviderReturnType --- extension.neon | 8 ++++---- rules.neon | 2 +- tests/Type/PHPUnit/data/data-provider-iterable-value.neon | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extension.neon b/extension.neon index 23ae52cf..4b2d9c54 100644 --- a/extension.neon +++ b/extension.neon @@ -1,7 +1,7 @@ parameters: phpunit: convertUnionToIntersectionType: true - checkDataProviderData: %featureToggles.bleedingEdge% + reportMissingDataProviderReturnType: false additionalConstructors: - PHPUnit\Framework\TestCase::setUp earlyTerminatingMethodCalls: @@ -24,8 +24,8 @@ parameters: parametersSchema: phpunit: structure([ - convertUnionToIntersectionType: bool() - checkDataProviderData: bool(), + convertUnionToIntersectionType: bool(), + reportMissingDataProviderReturnType: bool(), ]) services: @@ -76,4 +76,4 @@ conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: phpstan.phpDoc.typeNodeResolverExtension: %phpunit.convertUnionToIntersectionType% PHPStan\Type\PHPUnit\DataProviderReturnTypeIgnoreExtension: - phpstan.ignoreErrorExtension: %phpunit.checkDataProviderData% + phpstan.ignoreErrorExtension: [%featureToggles.bleedingEdge%, not(%phpunit.reportMissingDataProviderReturnType%)] diff --git a/rules.neon b/rules.neon index 8469bd1e..7bff0161 100644 --- a/rules.neon +++ b/rules.neon @@ -14,7 +14,7 @@ conditionalTags: phpstan.rules.rule: [%strictRulesInstalled%, %featureToggles.bleedingEdge%] PHPStan\Rules\PHPUnit\DataProviderDataRule: - phpstan.rules.rule: %phpunit.checkDataProviderData% + phpstan.rules.rule: %featureToggles.bleedingEdge% services: - diff --git a/tests/Type/PHPUnit/data/data-provider-iterable-value.neon b/tests/Type/PHPUnit/data/data-provider-iterable-value.neon index eed12a5b..e5597bc2 100644 --- a/tests/Type/PHPUnit/data/data-provider-iterable-value.neon +++ b/tests/Type/PHPUnit/data/data-provider-iterable-value.neon @@ -1,6 +1,6 @@ parameters: - phpunit: - checkDataProviderData: true + featureToggles: + bleedingEdge: true includes: - ../../../../extension.neon From 202afe95f3832c62b578f9bbeebba8b2e840260b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:46:46 +0000 Subject: [PATCH 276/277] chore(deps): update github-actions --- .github/workflows/build.yml | 4 ++-- .github/workflows/lock-closed-issues.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b74773b1..2df99361 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -271,7 +271,7 @@ jobs: echo "name=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p')" >> $GITHUB_OUTPUT - name: "Restore result cache" - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ./tmp key: "result-cache-v1-${{ matrix.php-version }}-${{ github.run_id }}" @@ -292,7 +292,7 @@ jobs: --logger-text=php://stdout - name: "Save result cache" - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 if: ${{ !cancelled() }} with: path: ./tmp diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 9a8fea7e..e5ac0704 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -8,7 +8,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5 + - uses: dessant/lock-threads@v6 with: github-token: ${{ github.token }} issue-inactive-days: '31' From 5e30669bef866eff70db8b58d72a5c185aa82414 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 19 Dec 2025 10:05:35 +0100 Subject: [PATCH 277/277] Perf: Refactor PHPUnitVersionDetector to use runtime reflection --- src/Rules/PHPUnit/PHPUnitVersionDetector.php | 51 ++++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/Rules/PHPUnit/PHPUnitVersionDetector.php b/src/Rules/PHPUnit/PHPUnitVersionDetector.php index b82caaf4..d35c3e72 100644 --- a/src/Rules/PHPUnit/PHPUnitVersionDetector.php +++ b/src/Rules/PHPUnit/PHPUnitVersionDetector.php @@ -2,45 +2,44 @@ namespace PHPStan\Rules\PHPUnit; -use PHPStan\Reflection\ReflectionProvider; use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionException; use function dirname; use function explode; use function file_get_contents; -use function is_file; use function json_decode; class PHPUnitVersionDetector { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) - { - $this->reflectionProvider = $reflectionProvider; - } - public function createPHPUnitVersion(): PHPUnitVersion { + $file = false; $majorVersion = null; $minorVersion = null; - if ($this->reflectionProvider->hasClass(TestCase::class)) { - $testCase = $this->reflectionProvider->getClass(TestCase::class); - $file = $testCase->getFileName(); - if ($file !== null) { - $phpUnitRoot = dirname($file, 3); - $phpUnitComposer = $phpUnitRoot . '/composer.json'; - if (is_file($phpUnitComposer)) { - $composerJson = @file_get_contents($phpUnitComposer); - if ($composerJson !== false) { - $json = json_decode($composerJson, true); - $version = $json['extra']['branch-alias']['dev-main'] ?? null; - if ($version !== null) { - $versionParts = explode('.', $version); - $majorVersion = (int) $versionParts[0]; - $minorVersion = (int) $versionParts[1]; - } - } + + try { + // uses runtime reflection to reduce unnecessary work while bootstrapping PHPStan. + // static reflection would need to AST parse and build up reflection for a lot of files otherwise. + $reflection = new ReflectionClass(TestCase::class); + $file = $reflection->getFileName(); + } catch (ReflectionException $e) { + // PHPUnit might not be installed + } + + if ($file !== false) { + $phpUnitRoot = dirname($file, 3); + $phpUnitComposer = $phpUnitRoot . '/composer.json'; + + $composerJson = @file_get_contents($phpUnitComposer); + if ($composerJson !== false) { + $json = json_decode($composerJson, true); + $version = $json['extra']['branch-alias']['dev-main'] ?? null; + if ($version !== null) { + $versionParts = explode('.', $version); + $majorVersion = (int) $versionParts[0]; + $minorVersion = (int) $versionParts[1]; } } }