diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 270f0f51..9299e527 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,7 +6,11 @@ on:
pull_request:
push:
branches:
- - "1.4.x"
+ - "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:
@@ -16,8 +20,6 @@ jobs:
strategy:
matrix:
php-version:
- - "7.2"
- - "7.3"
- "7.4"
- "8.0"
- "8.1"
@@ -27,7 +29,7 @@ jobs:
steps:
- name: "Checkout"
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -41,10 +43,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"
@@ -55,14 +53,14 @@ 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"
- ref: "1.x"
+ ref: "2.x"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -94,8 +92,6 @@ jobs:
fail-fast: false
matrix:
php-version:
- - "7.2"
- - "7.3"
- "7.4"
- "8.0"
- "8.1"
@@ -105,10 +101,34 @@ jobs:
dependencies:
- "lowest"
- "highest"
+ phpunit-version:
+ - "^9.5"
+ - "^10.5"
+ - "^11.5"
+ - "^12.0.9"
+ 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"
+ - 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"
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -116,6 +136,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"
@@ -124,10 +147,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 +158,6 @@ jobs:
fail-fast: false
matrix:
php-version:
- - "7.2"
- - "7.3"
- "7.4"
- "8.0"
- "8.1"
@@ -150,10 +167,34 @@ jobs:
dependencies:
- "lowest"
- "highest"
+ phpunit-version:
+ - "^9.5"
+ - "^10.5"
+ - "^11.5"
+ - "^12.0.9"
+ 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"
+ - 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"
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -163,6 +204,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"
@@ -171,9 +215,81 @@ 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"
+
+ 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 }}"
+ php-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: "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 }}"
+ restore-keys: |
+ result-cache-v1-${{ matrix.php-version }}-
+
+ - name: "Run infection"
+ run: |
+ git fetch --depth=1 origin ${{ steps.default-branch.outputs.name }}
+ infection \
+ --git-diff-base=origin/${{ steps.default-branch.outputs.name }} \
+ --git-diff-lines \
+ --ignore-msi-with-no-mutations \
+ --min-msi=100 \
+ --min-covered-msi=100 \
+ --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/.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-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
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b1a669a9..ed7e51ad 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,11 +14,11 @@ jobs:
steps:
- name: "Checkout"
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Generate changelog
id: changelog
- uses: metcalfc/changelog-generator@v4.3.1
+ uses: metcalfc/changelog-generator@v4.6.2
with:
myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }}
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
diff --git a/Makefile b/Makefile
index b01b1537..c58ca06e 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
@@ -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
.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 -b phpstan-baseline.neon
diff --git a/README.md b/README.md
index 205cbe4b..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.
@@ -22,21 +22,20 @@ 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)
+* Check that you are not using `assertNotEquals()` with same types (`assertNotSame()` should be used)
## 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'));
@@ -44,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
diff --git a/composer.json b/composer.json
index 5b1ec505..39d7a030 100644
--- a/composer.json
+++ b/composer.json
@@ -6,22 +6,20 @@
"MIT"
],
"require": {
- "php": "^7.2 || ^8.0",
- "phpstan/phpstan": "^1.12"
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.1.32"
},
"conflict": {
"phpunit/phpunit": "<7.0"
},
"require-dev": {
- "nikic/php-parser": "^4.13.0",
+ "nikic/php-parser": "^5",
"php-parallel-lint/php-parallel-lint": "^1.2",
- "phpstan/phpstan-strict-rules": "^1.5.1",
- "phpunit/phpunit": "^9.5"
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6"
},
"config": {
- "platform": {
- "php": "7.4.6"
- },
"sort-packages": true
},
"extra": {
diff --git a/extension.neon b/extension.neon
index 8de21f54..23ae52cf 100644
--- a/extension.neon
+++ b/extension.neon
@@ -1,6 +1,7 @@
parameters:
phpunit:
convertUnionToIntersectionType: true
+ checkDataProviderData: %featureToggles.bleedingEdge%
additionalConstructors:
- PHPUnit\Framework\TestCase::setUp
earlyTerminatingMethodCalls:
@@ -12,7 +13,6 @@ parameters:
- stubs/Assert.stub
- stubs/AssertionFailedError.stub
- stubs/ExpectationFailedException.stub
- - stubs/InvocationMocker.stub
- stubs/MockBuilder.stub
- stubs/MockObject.stub
- stubs/Stub.stub
@@ -25,6 +25,7 @@ parameters:
parametersSchema:
phpunit: structure([
convertUnionToIntersectionType: bool()
+ checkDataProviderData: bool(),
])
services:
@@ -42,28 +43,37 @@ 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
-
class: PHPStan\Rules\PHPUnit\AnnotationHelper
+
+ -
+ class: PHPStan\Rules\PHPUnit\TestMethodsHelper
+
+ -
+ class: PHPStan\Rules\PHPUnit\PHPUnitVersion
+ factory: @PHPStan\Rules\PHPUnit\PHPUnitVersionDetector::createPHPUnitVersion()
+ -
+ class: PHPStan\Rules\PHPUnit\PHPUnitVersionDetector
+
-
class: PHPStan\Rules\PHPUnit\DataProviderHelper
factory: @PHPStan\Rules\PHPUnit\DataProviderHelperFactory::create()
-
class: PHPStan\Rules\PHPUnit\DataProviderHelperFactory
+ 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/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/phpstan.neon b/phpstan.neon
index 2b8fa1ae..7b35ce80 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -2,9 +2,24 @@ 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
parameters:
+ level: 8
+ reportUnmatchedIgnoredErrors: false
+
+ resultCachePath: tmp/resultCache.php
+
+ paths:
+ - src
+ - tests
+
excludePaths:
- tests/*/data/*
+ ignoreErrors:
+ -
+ message: '#^Attribute class PHPUnit\\Framework\\Attributes\\DataProvider does not exist\.$#'
+ identifier: attribute.notFound
+ reportUnmatched: false
diff --git a/phpunit.xml b/phpunit.xml
index 8f71615a..2ccef9fc 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -10,22 +10,9 @@
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
- xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xml"
+ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
+ executionOrder="random"
>
-
-
- ./src
-
-
-
-
-
-
-
tests
diff --git a/rules.neon b/rules.neon
index 8dc7056b..8272f47a 100644
--- a/rules.neon
+++ b/rules.neon
@@ -2,28 +2,31 @@ 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
+conditionalTags:
+ PHPStan\Rules\PHPUnit\AssertEqualsIsDiscouragedRule:
+ phpstan.rules.rule: [%strictRulesInstalled%, %featureToggles.bleedingEdge%]
+
+ PHPStan\Rules\PHPUnit\DataProviderDataRule:
+ phpstan.rules.rule: %phpunit.checkDataProviderData%
+
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
+ tags:
+ - phpstan.rules.rule
-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%
+ -
+ class: PHPStan\Rules\PHPUnit\AssertEqualsIsDiscouragedRule
+
+ -
+ class: PHPStan\Rules\PHPUnit\DataProviderDataRule
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/AssertEqualsIsDiscouragedRule.php b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php
new file mode 100644
index 00000000..ed6470e2
--- /dev/null
+++ b/src/Rules/PHPUnit/AssertEqualsIsDiscouragedRule.php
@@ -0,0 +1,87 @@
+
+ */
+class AssertEqualsIsDiscouragedRule implements Rule
+{
+
+ public function getNodeType(): string
+ {
+ return CallLike::class;
+ }
+
+ public function processNode(Node $node, Scope $scope): array
+ {
+ if (!$node instanceof Node\Expr\MethodCall && ! $node instanceof Node\Expr\StaticCall) {
+ return [];
+ }
+ if (count($node->getArgs()) < 2) {
+ return [];
+ }
+ if ($node->isFirstClassCallable()) {
+ return [];
+ }
+
+ if (
+ !$node->name instanceof Node\Identifier
+ || !in_array(strtolower($node->name->name), ['assertequals', 'assertnotequals'], true)
+ ) {
+ 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));
+
+ 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())
+ ) {
+ $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',
+ $correctName,
+ $node->name->name,
+ ),
+ )->identifier('phpunit.assertEquals')
+ ->fixNode($node, static function (CallLike $node) use ($correctName) {
+ $node->name = new Node\Identifier($correctName);
+
+ return $node;
+ })
+ ->build(),
+ ];
+ }
+
+ return [];
+ }
+
+}
diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php
index 3ad79c00..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) {
@@ -30,7 +27,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/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php
index 308f5147..f185bdf7 100644
--- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php
+++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php
@@ -23,13 +23,15 @@ 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 (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') {
return [];
}
@@ -39,19 +41,52 @@ 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(),
+ 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/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php
index 363fa578..f6032efb 100644
--- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php
+++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php
@@ -23,17 +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 (count($node->getArgs()) < 2) {
return [];
}
+ 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 [];
@@ -41,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/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php
index 3c3ada4b..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
@@ -24,24 +28,25 @@ 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->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') {
+ if ($node->isFirstClassCallable()) {
+ return [];
+ }
+ if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') {
return [];
}
- $right = $node->getArgs()[1]->value;
+ if (!AssertRuleHelper::isMethodOrStaticCallOnAssert($node, $scope)) {
+ return [];
+ }
- if (
- $right instanceof Node\Expr\FuncCall
- && $right->name instanceof Node\Name
- && $right->name->toLowerString() === 'count'
- ) {
+ $right = $node->getArgs()[1]->value;
+ if (self::isCountFunctionCall($right, $scope)) {
return [
RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).')
->identifier('phpunit.assertCount')
@@ -49,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/src/Rules/PHPUnit/ClassCoversExistsRule.php b/src/Rules/PHPUnit/ClassCoversExistsRule.php
index bbced022..a36317ef 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,
@@ -52,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 [];
}
@@ -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..dd328f83 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,
@@ -58,16 +56,14 @@ public function processNode(Node $node, Scope $scope): array
return [];
}
- if (!$classReflection->isSubclassOf(TestCase::class)) {
+ if (!$classReflection->is(TestCase::class)) {
return [];
}
$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/DataProviderDataRule.php b/src/Rules/PHPUnit/DataProviderDataRule.php
new file mode 100644
index 00000000..ce994676
--- /dev/null
+++ b/src/Rules/PHPUnit/DataProviderDataRule.php
@@ -0,0 +1,250 @@
+
+ */
+class DataProviderDataRule implements Rule
+{
+
+ private TestMethodsHelper $testMethodsHelper;
+
+ private DataProviderHelper $dataProviderHelper;
+
+ private PHPUnitVersion $PHPUnitVersion;
+
+ public function __construct(
+ TestMethodsHelper $testMethodsHelper,
+ DataProviderHelper $dataProviderHelper,
+ PHPUnitVersion $PHPUnitVersion
+ )
+ {
+ $this->testMethodsHelper = $testMethodsHelper;
+ $this->dataProviderHelper = $dataProviderHelper;
+ $this->PHPUnitVersion = $PHPUnitVersion;
+ }
+
+ 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) {
+ 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 = null;
+ foreach ($testsWithProvider as $testMethod) {
+ $num = $testMethod->getNumberOfParameters();
+ if ($testMethod->isVariadic()) {
+ $num = PHP_INT_MAX;
+ }
+ if ($maxNumberOfParameters === null) {
+ $maxNumberOfParameters = $num;
+ continue;
+ }
+
+ $maxNumberOfParameters = max($maxNumberOfParameters, $num);
+ if ($num === PHP_INT_MAX) {
+ break;
+ }
+ }
+
+ foreach ($testsWithProvider as $testMethod) {
+ $numberOfParameters = $testMethod->getNumberOfParameters();
+
+ foreach ($arraysTypes as [$startLine, $arraysType]) {
+ $args = $this->arrayItemsToArgs($arraysType, $numberOfParameters);
+ 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 || !$this->PHPUnitVersion->supportsNamedArgumentsInDataProvider()->yes()) {
+ $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/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php
index 37c586de..1983493c 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,
@@ -55,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 [];
}
@@ -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..d40d05e4 100644
--- a/src/Rules/PHPUnit/DataProviderHelper.php
+++ b/src/Rules/PHPUnit/DataProviderHelper.php
@@ -2,12 +2,16 @@
namespace PHPStan\Rules\PHPUnit;
+use PhpParser\Comment\Doc;
+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;
@@ -16,94 +20,56 @@
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.
- *
- * @var ReflectionProvider
- */
- private $reflectionProvider;
+ private ReflectionProvider $reflectionProvider;
- /**
- * The file type mapper.
- *
- * @var FileTypeMapper
- */
- private $fileTypeMapper;
+ private FileTypeMapper $fileTypeMapper;
- /** @var bool */
- private $phpunit10OrNewer;
+ private Parser $parser;
+
+ private PHPUnitVersion $PHPUnitVersion;
public function __construct(
ReflectionProvider $reflectionProvider,
FileTypeMapper $fileTypeMapper,
- bool $phpunit10OrNewer
+ Parser $parser,
+ PHPUnitVersion $PHPUnitVersion
)
{
$this->reflectionProvider = $reflectionProvider;
$this->fileTypeMapper = $fileTypeMapper;
- $this->phpunit10OrNewer = $phpunit10OrNewer;
+ $this->parser = $parser;
+ $this->PHPUnitVersion = $PHPUnitVersion;
}
/**
+ * @param ReflectionMethod|ClassMethod $testMethod
+ *
* @return iterable
*/
public function getDataProviderMethods(
Scope $scope,
- ClassMethod $node,
+ $testMethod,
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;
- }
+ yield from $this->yieldDataProviderAnnotations($testMethod, $scope, $classReflection);
- $dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue);
- $dataProviderMethod[] = $node->getLine();
-
- yield $dataProviderValue => $dataProviderMethod;
- }
- }
-
- if (!$this->phpunit10OrNewer) {
+ if (!$this->PHPUnitVersion->supportsDataProviderAttribute()->yes()) {
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($testMethod, $classReflection);
}
/**
@@ -122,7 +88,7 @@ private function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
foreach ($phpDocNodes as $docNode) {
$annotations = array_merge(
$annotations,
- $docNode->getTagsByName('@dataProvider')
+ $docNode->getTagsByName('@dataProvider'),
);
}
@@ -145,7 +111,7 @@ public function processDataProvider(
return [
RuleErrorBuilder::message(sprintf(
'@dataProvider %s related class not found.',
- $dataProviderValue
+ $dataProviderValue,
))
->line($lineNumber)
->identifier('phpunit.dataProviderClass')
@@ -159,7 +125,7 @@ public function processDataProvider(
return [
RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method not found.',
- $dataProviderValue
+ $dataProviderValue,
))
->line($lineNumber)
->identifier('phpunit.dataProviderMethod')
@@ -173,7 +139,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,21 +149,40 @@ public function processDataProvider(
if (!$dataProviderMethodReflection->isPublic()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method must be public.',
- $dataProviderValue
+ $dataProviderValue,
))
->line($lineNumber)
->identifier('phpunit.dataProviderPublic')
->build();
}
- if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) {
- $errors[] = RuleErrorBuilder::message(sprintf(
+ 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
+ $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;
@@ -260,13 +245,13 @@ private function parseDataProviderExternalAttribute(Attribute $attribute): ?arra
sprintf('%s::%s', $className, $methodNameArg->value) => [
$dataProviderClassReflection,
$methodNameArg->value,
- $attribute->getLine(),
+ $attribute->getStartLine(),
],
];
}
/**
- * @return array|null
+ * @return array|null
*/
private function parseDataProviderAttribute(Attribute $attribute, ClassReflection $classReflection): ?array
{
@@ -282,9 +267,96 @@ private function parseDataProviderAttribute(Attribute $attribute, ClassReflectio
$methodNameArg->value => [
$classReflection,
$methodNameArg->value,
- $attribute->getLine(),
+ $attribute->getStartLine(),
],
];
}
+ /**
+ * @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 7fc8af0f..33bbe22d 100644
--- a/src/Rules/PHPUnit/DataProviderHelperFactory.php
+++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php
@@ -2,56 +2,37 @@
namespace PHPStan\Rules\PHPUnit;
+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
{
- /** @var ReflectionProvider */
- private $reflectionProvider;
+ private ReflectionProvider $reflectionProvider;
- /** @var FileTypeMapper */
- private $fileTypeMapper;
+ private FileTypeMapper $fileTypeMapper;
- public function __construct(ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper)
+ private Parser $parser;
+
+ private PHPUnitVersion $PHPUnitVersion;
+
+ public function __construct(
+ ReflectionProvider $reflectionProvider,
+ FileTypeMapper $fileTypeMapper,
+ Parser $parser,
+ PHPUnitVersion $PHPUnitVersion
+ )
{
$this->reflectionProvider = $reflectionProvider;
$this->fileTypeMapper = $fileTypeMapper;
+ $this->parser = $parser;
+ $this->PHPUnitVersion = $PHPUnitVersion;
}
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, $phpUnit10OrNewer);
+ return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $this->parser, $this->PHPUnitVersion);
}
}
diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php
index f93ef167..6c3b0dc4 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,46 +48,60 @@ 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 function (string $class): bool {
- return $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($scope, $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($scope, $varType, $method);
+ if ($error === null) {
+ continue;
+ }
+
+ $errors[] = $error;
+ }
+
+ return $errors;
+ }
+
+ 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)
+ ) {
+ $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/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php b/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php
index 89e3e8ff..a2fc39f1 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)
{
@@ -34,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 77577206..906e60b1 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)
{
@@ -34,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/PHPUnitVersion.php b/src/Rules/PHPUnit/PHPUnitVersion.php
new file mode 100644
index 00000000..b7259a84
--- /dev/null
+++ b/src/Rules/PHPUnit/PHPUnitVersion.php
@@ -0,0 +1,49 @@
+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);
+ }
+
+ public function supportsNamedArgumentsInDataProvider(): TrinaryLogic
+ {
+ if ($this->majorVersion === null) {
+ return TrinaryLogic::createMaybe();
+ }
+ return TrinaryLogic::createFromBoolean($this->majorVersion >= 11);
+ }
+
+}
diff --git a/src/Rules/PHPUnit/PHPUnitVersionDetector.php b/src/Rules/PHPUnit/PHPUnitVersionDetector.php
new file mode 100644
index 00000000..f0e2c4b9
--- /dev/null
+++ b/src/Rules/PHPUnit/PHPUnitVersionDetector.php
@@ -0,0 +1,48 @@
+reflectionProvider = $reflectionProvider;
+ }
+
+ public function createPHPUnitVersion(): PHPUnitVersion
+ {
+ $majorVersion = 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) {
+ $majorVersion = (int) explode('.', $version)[0];
+ }
+ }
+ }
+ }
+ }
+
+ return new PHPUnitVersion($majorVersion);
+ }
+
+}
diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php
index 917d0bf9..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 [];
}
@@ -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/Rules/PHPUnit/TestMethodsHelper.php b/src/Rules/PHPUnit/TestMethodsHelper.php
new file mode 100644
index 00000000..5eb274e0
--- /dev/null
+++ b/src/Rules/PHPUnit/TestMethodsHelper.php
@@ -0,0 +1,99 @@
+fileTypeMapper = $fileTypeMapper;
+ $this->PHPUnitVersion = $PHPUnitVersion;
+ }
+
+ /**
+ * @return array
+ */
+ public function getTestMethods(ClassReflection $classReflection, Scope $scope): array
+ {
+ if (!$classReflection->is(TestCase::class)) {
+ return [];
+ }
+
+ $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->PHPUnitVersion->supportsTestAttribute()->no()) {
+ 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/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..04def4e3 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,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);
}
/**
@@ -136,95 +135,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 +247,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/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/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/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
deleted file mode 100644
index 4f74fe68..00000000
--- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php
+++ /dev/null
@@ -1,45 +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 function (string $class): bool {
- return $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/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
*/
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 @@
-
+ */
+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');
+ }
+
+}
diff --git a/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php
new file mode 100644
index 00000000..040c1eee
--- /dev/null
+++ b/tests/Rules/PHPUnit/AssertEqualsIsDiscouragedRuleTest.php
@@ -0,0 +1,47 @@
+
+ */
+final class AssertEqualsIsDiscouragedRuleTest extends RuleTestCase
+{
+
+ 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_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],
+ ]);
+ }
+
+ 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/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/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/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/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),
);
}
diff --git a/tests/Rules/PHPUnit/DataProviderDataRuleTest.php b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php
new file mode 100644
index 00000000..cca88e7f
--- /dev/null
+++ b/tests/Rules/PHPUnit/DataProviderDataRuleTest.php
@@ -0,0 +1,356 @@
+
+ */
+class DataProviderDataRuleTest extends RuleTestCase
+{
+ 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),
+ $phpunitVersion
+ ),
+ new DataProviderHelper(
+ $reflectionProvider,
+ self::getContainer()->getByType(FileTypeMapper::class),
+ self::getContainer()->getService('defaultAnalysisParser'),
+ $phpunitVersion
+ ),
+ $phpunitVersion,
+ ),
+ self::getContainer()->getByType(CallMethodsRule::class) /** @phpstan-ignore phpstanApi.classConstant */
+ ];
+
+ return new CompositeRule($rules);
+ }
+
+ 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.',
+ 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,
+ ],
+ ]);
+ }
+
+
+ /**
+ * @dataProvider provideNamedArgumentPHPUnitVersions
+ */
+ #[DataProvider('provideNamedArgumentPHPUnitVersions')]
+ public function testRulePhp8(?int $phpunitVersion): void
+ {
+ if (PHP_VERSION_ID < 80000) {
+ self::markTestSkipped();
+ }
+
+ $this->phpunitVersion = $phpunitVersion;
+
+ 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);
+ }
+
+
+ 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.',
+ 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->phpunitVersion = 10;
+
+ $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,
+ ],
+ [
+ 'Parameter #6 ...$m of method DataProviderTrimmingArgs\BazTest::testProvide() expects int, string given.',
+ 90,
+ ],
+ ]);
+ }
+
+ 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[]
+ */
+ public static function getAdditionalConfigFiles(): array
+ {
+ return [
+ __DIR__ . '/../../../extension.neon',
+ ];
+ }
+}
diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php
index 18cc12b3..2bf9d870 100644
--- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php
+++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php
@@ -5,64 +5,117 @@
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
{
$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'),
+ new PHPUnitVersion($this->phpunitVersion)
+ ),
true,
true
);
}
- public function testRule(): void
+ /**
+ * @dataProvider provideVersions
+ */
+ #[DataProvider('provideVersions')]
+ public function testRule(?int $version): void
+ {
+ $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
{
- $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,
- ],
- ]);
+ 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');
}
/**
diff --git a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php
index c9c33e6f..f7e89c7a 100644
--- a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php
+++ b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php
@@ -4,7 +4,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
-use function interface_exists;
+use const PHP_VERSION_ID;
/**
* @extends RuleTestCase
@@ -28,18 +28,23 @@ 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);
}
+ public function testBug227(): void
+ {
+ if (PHP_VERSION_ID < 80000) {
+ self::markTestSkipped('Test requires PHP 8.0.');
+ }
+ $this->analyse([__DIR__ . '/data/bug-227.php'], []);
+ }
+
/**
* @return string[]
*/
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);
+ }
+
+}
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..7f4d80e1
--- /dev/null
+++ b/tests/Rules/PHPUnit/data/assert-equals-is-discouraged.php
@@ -0,0 +1,43 @@
+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());
+
+ $this->assertNotEquals($string, $string);
+ $this->assertNotEquals($integer, $integer);
+ $this->assertNotEquals($boolean, $boolean);
+ $this->assertNotSame(5, $integer);
+ static::assertNotSame(5, $integer);
+ }
+}
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());
+ }
+
+}
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;
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;
}
-};
+}
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');
+ }
+
+}
diff --git a/tests/Rules/PHPUnit/data/bug-227.php b/tests/Rules/PHPUnit/data/bug-227.php
new file mode 100644
index 00000000..1efe6c1c
--- /dev/null
+++ b/tests/Rules/PHPUnit/data/bug-227.php
@@ -0,0 +1,42 @@
+= 8.0
+
+namespace Bug227;
+
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use stdClass;
+
+class Foo
+{
+
+ public function addCacheTags(array $tags)
+ {
+
+ }
+
+ public function getLanguage(): stdClass
+ {
+
+ }
+
+}
+
+class SomeTest extends TestCase
+{
+
+ protected MockObject|Foo $tsfe;
+
+ protected function setUp(): void
+ {
+ $this->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');
+ }
+}
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-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,
+ ];
+ }
+}
+
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 @@
+>
+ */
+ 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
+ {
+
+ }
+
+}
+
+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'],
+ ];
+ }
+
+}
diff --git a/tests/Rules/PHPUnit/data/data-provider-variadic-method.php b/tests/Rules/PHPUnit/data/data-provider-variadic-method.php
new file mode 100644
index 00000000..978d197b
--- /dev/null
+++ b/tests/Rules/PHPUnit/data/data-provider-variadic-method.php
@@ -0,0 +1,61 @@
+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 [];
@@ -26,6 +27,7 @@ public 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 e1841e0b..8c6ebb8b 100644
--- a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php
+++ b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php
@@ -3,20 +3,22 @@
namespace PHPStan\Type\PHPUnit;
use PHPStan\Testing\TypeInferenceTestCase;
+use PHPUnit\Framework\Attributes\DataProvider;
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');
}
/**
* @dataProvider dataFileAsserts
* @param mixed ...$args
*/
+ #[DataProvider('dataFileAsserts')]
public function testFileAsserts(
string $assertType,
string $file,
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/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php
index 10fb4176..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
{
@@ -38,7 +37,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,16 +46,16 @@ 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);
}
- public function objectHasAttribute(object $a): void
+ public function arrayHasExprKey(int $index, array $a): void
{
- assertObjectHasAttribute('property', $a);
- assertType("object&hasProperty(property)", $a);
+ assertArrayHasKey($index, $a);
+ assertType("non-empty-array", $a);
}
public function testEmpty($a): void
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 @@
+