Skip to content

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Oct 13, 2025

Proof of concept

<?php

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class test extends TestCase
{

	#[DataProvider('aProvider')]
	public function testTrim(string $expectedResult, string $input): void
	{
	}

	public function aProvider(): array /** @phpstan-ignore missingType.iterableValue */
	{
		return [
			[
				'Hello World',
				" Hello World \n",
			],
			[
				'Hello World',
				123,
			],
			[
				'Hello World',
				false,
			],
		];
	}
}

leads to

 ------ ----------------------------------------------------------------------------- 
  Line   test.php                                                                     
 ------ ----------------------------------------------------------------------------- 
  21     Parameter #2 $input of method test::testTrim() expects string, int given.    
         🪪  argument.type                                                            
         at test.php:21                                                               
  25     Parameter #2 $input of method test::testTrim() expects string, false given.  
         🪪  argument.type                                                            
         at test.php:25                                                               
 ------ ----------------------------------------------------------------------------- 

Closes #70

utilizes phpstan/phpstan-src#4429 phpstan/phpstan-src#4438


Scope

  • same class dataprovider only for now

Todos

  • POC
  • Support @test
  • Support #[Test]
  • Support "test*" method name prefix
  • Support @dataProvider
  • Support #[DataProvider]
  • Support static data-provider
  • Support non-static data-provider
  • Support return [] data-providers
  • Support yield [] data-providers
  • Support yield from [] data-providers
  • named arguments in data-provider
  • Bleeding Edge only
  • variadic parameters in test-methods
  • check number of arguments vs. parameters
  • Port CompositionRule into phpstan-src

@staabm
Copy link
Contributor Author

staabm commented Oct 14, 2025

Still work in progress. Will separate into more PRs after its mostly done

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I pushed a bunch of failing tests 😊

}

/**
* @return array<ReflectionMethod>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a weird type. I'd prefer our own ExtendedMethodReflection to be returned. You can get ClassReflection::getNativeMethod() once you make sure this is a test method from the current $reflectionMethod (to save time from instantiating all methods which is expensive).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this because I was looking for how you're handling variadic methods and the $testMethod->getNumberOfParameters() method call seemed unfamiliar to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from ExtendedMethodReflection I cannot getStartLine, which I need in DataProviderHelper

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean there isn't getStartLine on AttributeReflection? I don't see getStartLine being called on ReflectionMethod.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see

$startLine = $node->getStartLine();

}

$maxNumberOfParameters = 0;
$trimArgs = count($testsWithProvider) > 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in correct implementation, we don't need this condition at all. A foreach over methods that just iterates once should be enough to set everything correctly.

Copy link
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about code like

    #[DataProvider('aProvider')]
	public function testTrim(string $expectedResult, string $input): void
	{
	}

    /** @return array<array{string, string|int|false}> */
	public function aProvider(): array
	{
		return [
			[
				'Hello World',
				" Hello World \n",
			],
			[
				'Hello World',
				123,
			],
			[
				'Hello World',
				false,
			],
		];
	}

@staabm ?

I would expect:

  • To not parse the implementation of the provider because I added a return type (which improves performance of the rule).
  • /** @return array<array{string, string|int|false}> */ to be reported because it's not compatible with the testTrim signature.

This way:

  • I fix the phpdoc to /** @return array<array{string, string}> */
  • And then, the 123 and false will be reported because they don't respect the @return type.

@staabm
Copy link
Contributor Author

staabm commented Oct 17, 2025

I think with the new rule, you no longer need a return type on the provider (which is now redundant). having to define the signature at the test and another time at the provider feels like unnecessary work

IMO at best we would stop reporting "return type has no value type specified in iterable type array." for data-providers which can be analyzed by this PR.

@VincentLanglet
Copy link
Contributor

I think with the new rule, you no longer need a return type on the provider (which is now redundant). having to define the signature at the test and another time at the provider fells like unnecessary work

IMO at best we would stop reporting "return type has no value type specified in iterable type array." for data-providers which can be analyzed by this PR.

I feel like it's a really opinionated decision, and might conflict with some other static analysis tool.

  • I think psalm use the phpdoc from the dataprovider for his "similar" DataProviderDataRule.
  • DataProvider might be extended/overriden and then I'll want to add phpdoc to restrict/extends the return type.
  • Team could have a 100% typed policy (like we do)

Couldn't the DataProviderDataRule either

  • Skip provider with phpdoc (and another rule will do the check)
  • Or rely only on the phpdoc

This rule will

Cause some code like

    #[DataProvider('aProvider')]
	public function testTrim(string $expectedResult, string $input): void
	{
	}

    /** @return array<array{string, string|int}> */
	public function aProvider(): array
	{
		return [
			[
				'Hello World',
				" Hello World \n",
			],
		];
	}

Is not reported by the rule currently, but technically wrong (and reported by Psalm)

@ondrejmirtes
Copy link
Member

@VincentLanglet I disagree with you but I'm on a phone and might write a proper response latee.

Comment on lines +63 to +70
$method = $scope->getFunction();
$classReflection = $scope->getClassReflection();
if (
$classReflection === null
|| !$classReflection->is(TestCase::class)
) {
return [];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this condition be done before buildArrayTypesFromNode to be faster ?

We might also prefer to check

if (count($testsWithProvider) === 0) {
     return [];
}

before the buildArrayTypesFromNode call, it will avoid to inspect methods which are not dataprovider and improves performances.

Copy link
Contributor Author

@staabm staabm Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure whether AST traversing + type retrieval is faster or slower then runtime reflection.
do you have a test at hand which to prove your thesis about what is faster?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure whether AST traversing + type retrieval is faster or slower then runtime reflection.
do you have a cast at hand which to prove your thesis about what is faster?

Not at all, it might be a wrong intuition.
I thought type manipulation was something slow, with possible union/intersection etc.

$testsWithProvider = [];
$testMethods = $this->testMethodsHelper->getTestMethods($classReflection, $scope);
foreach ($testMethods as $testMethod) {
foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $testMethod, $classReflection) as [, $providerMethodName]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be worth having a cache on getDataProviderMethods (and maybe getTestMethods ?) ?

Cause we will compute this for every possible return of a TestCase class, and the result will always be the same

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine/willing to add a cache in case we find a case where it turns into a measurable improvement

@ondrejmirtes
Copy link
Member

@VincentLanglet To reply to your earlier comment about this code:

    #[DataProvider('aProvider')]
	public function testTrim(string $expectedResult, string $input): void
	{
	}

    /** @return array<array{string, string|int}> */
	public function aProvider(): array
	{
		return [
			[
				'Hello World',
				" Hello World \n",
			],
		];
	}

Currently this code will be reported with return.nestedUnusedType (https://phpstan.org/r/3b73fe7f-361e-4f8e-a752-0cf8827db30e). Provided it's in a final class or you turn on checkTooWideReturnTypesInProtectedAndPublicMethods.

@staabm
Copy link
Contributor Author

staabm commented Oct 18, 2025

Also I pushed a bunch of failing tests 😊

fixed, thank you 🙏

@ondrejmirtes
Copy link
Member

I played as Infection for a bit and found some code that could be removed from the algorithm 😊 I still think there's something fishy about it but I didn't prove it yet. In my gut I think we should be interesting in the min number of parameters when deciding how to trim, but maybe I'm wrong, or maybe I will find another failing test :) We'll see. Thank you so far!

@staabm
Copy link
Contributor Author

staabm commented Oct 18, 2025

In my gut I think we should be interesting in the min number of parameters when deciding how to trim

thats what you deleted with f083e63 ...? :)

@staabm
Copy link
Contributor Author

staabm commented Oct 18, 2025

I played as Infection for a bit

understood. finished the infection package, so we can setup it :)

phpstan/build-infection#1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

typecheck dataproviders

3 participants