Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
37a7dc6
fix(dead-code): mark PHPUnit hook methods as used #251
alies-dev Nov 10, 2026
9de65a6
fix(provider): report argument type coercion like Psalm core #248
alies-dev Nov 10, 2026
fed591d
fix(missing-constructor): honour setUp/#[Before] from traits #252
alies-dev Nov 10, 2026
1bbc943
fix(provider): check datasets of iterable<V> providers #246
alies-dev Nov 10, 2026
dfbb7d5
fix(provider): expand type aliases of test params in dataset checks #249
alies-dev Nov 10, 2026
34b5864
fix(tests): ignore non-public methods as tests #250
alies-dev Nov 10, 2026
1830729
Merge remote-tracking branch 'origin/master' into fix/246-iterable-key
alies-dev Nov 10, 2026
e943a07
Merge remote-tracking branch 'origin/master' into fix/248-argument-co…
alies-dev Nov 10, 2026
eb1c10f
Merge remote-tracking branch 'origin/master' into fix/249-type-alias-…
alies-dev Nov 10, 2026
c9cf83c
Merge remote-tracking branch 'origin/master' into fix/251-hook-methods
alies-dev Nov 10, 2026
4bc80b0
Merge remote-tracking branch 'origin/master' into fix/252-trait-initi…
alies-dev Nov 10, 2026
4201b14
Merge remote-tracking branch 'origin/master' into fix/250-non-public-…
alies-dev Nov 10, 2026
4690926
fix(dataset): accept open list datasets for further parameters #247
alies-dev Nov 10, 2026
0827710
Merge remote-tracking branch 'origin/master' into fix/247-list-datasets
alies-dev Nov 10, 2026
04c337e
refactor(dead-code): simplify hook detection, cover attribute precede…
alies-dev Nov 10, 2026
b0f403c
fix(provider): simplify mixed key exemption, add guard scenarios #246
alies-dev Nov 10, 2026
d858042
fix(provider): resolve trait visibility of tests via appearing class …
alies-dev Nov 10, 2026
a76ca27
fix(provider): keep null/false findings next to coercion #248
alies-dev Nov 10, 2026
93b10a1
fix(provider): keep open list tails to arity and dependency bounds #247
alies-dev Nov 10, 2026
cb0517b
fix(missing-constructor): walk each used trait once #252
alies-dev Nov 10, 2026
9ec927f
style: apply phpcs fixes #248
alies-dev Nov 10, 2026
5775d50
style: apply phpcs fixes #252
alies-dev Nov 10, 2026
97fcded
fix(provider): expand self and aliases in provider checks per class #249
alies-dev Nov 10, 2026
a1962eb
fix(provider): accept any class name string when expanding types #249
alies-dev Nov 10, 2026
253858c
Merge remote-tracking branch 'origin/master' into fix/246-iterable-key
alies-dev Nov 10, 2026
6cf9f30
Merge remote-tracking branch 'origin/master' into fix/247-list-datasets
alies-dev Nov 10, 2026
54b131b
Merge remote-tracking branch 'origin/master' into fix/248-argument-co…
alies-dev Nov 10, 2026
eccbc05
Merge remote-tracking branch 'origin/master' into fix/249-type-alias-…
alies-dev Nov 10, 2026
b313f63
Merge remote-tracking branch 'origin/master' into fix/250-non-public-…
alies-dev Nov 10, 2026
6f28e88
Merge remote-tracking branch 'origin/master' into fix/251-hook-methods
alies-dev Nov 10, 2026
ef187b5
Merge remote-tracking branch 'origin/master' into fix/252-trait-initi…
alies-dev Nov 10, 2026
cc33349
Merge branch 'fix/251-hook-methods' into fix/252-trait-initializers
alies-dev Nov 10, 2026
3e459c8
Merge branch 'fix/252-trait-initializers' into fix/250-non-public-tests
alies-dev Nov 10, 2026
255f3ef
Merge branch 'fix/250-non-public-tests' into fix/246-iterable-key
alies-dev Nov 10, 2026
24a3be0
Merge branch 'fix/246-iterable-key' into fix/247-list-datasets
alies-dev Nov 10, 2026
dc348c0
Merge branch 'fix/247-list-datasets' into fix/248-argument-coercion
alies-dev Nov 10, 2026
1f99b40
Merge branch 'fix/248-argument-coercion' into fix/249-type-alias-params
alies-dev Nov 10, 2026
94bf8f9
test(provider): expect coercion for alias datasets after #248 #249
alies-dev Nov 10, 2026
f0a5065
Merge remote-tracking branch 'origin/master' into fix/251-hook-methods
alies-dev Nov 10, 2026
b1bb6ce
Merge branch 'fix/251-hook-methods' into fix/252-trait-initializers
alies-dev Nov 10, 2026
42cec42
Merge branch 'fix/252-trait-initializers' into fix/250-non-public-tests
alies-dev Nov 10, 2026
8c526d9
Merge branch 'fix/250-non-public-tests' into fix/246-iterable-key
alies-dev Nov 10, 2026
df69574
Merge branch 'fix/246-iterable-key' into fix/247-list-datasets
alies-dev Nov 10, 2026
bd5eafa
Merge branch 'fix/247-list-datasets' into fix/248-argument-coercion
alies-dev Nov 10, 2026
610c069
test(provider): honour suppressions of argument coercions #248
alies-dev Nov 10, 2026
2c5dfb6
Merge branch 'fix/248-argument-coercion' into fix/249-type-alias-params
alies-dev Nov 10, 2026
b466cdf
Reuse the computed specials to detect hook methods
alies-dev Nov 10, 2026
dccbc07
Find initializers through the methods Psalm merged into the class
alies-dev Nov 10, 2026
b236802
Build the provider argument messages once
alies-dev Nov 10, 2026
8d89598
Share the self/static expansion of provider types with the narrowing …
alies-dev Nov 10, 2026
54dcf04
Merge remote-tracking branch 'origin/master' into fix/251-hook-methods
alies-dev Nov 10, 2026
5117aab
Use the required parameter count Psalm already records
alies-dev Nov 10, 2026
b44d005
Merge branch 'fix/251-hook-methods' into fix/252-trait-initializers
alies-dev Nov 10, 2026
a8c4844
Merge remote-tracking branch 'origin/master' into fix/252-trait-initi…
alies-dev Nov 10, 2026
fa633f2
Merge branch 'fix/252-trait-initializers' into fix/250-non-public-tests
alies-dev Nov 10, 2026
5d4eaf9
Merge branch 'fix/250-non-public-tests' into fix/246-iterable-key
alies-dev Nov 10, 2026
1c4dc51
Merge branch 'fix/246-iterable-key' into fix/247-list-datasets
alies-dev Nov 10, 2026
da902aa
Merge branch 'fix/247-list-datasets' into fix/248-argument-coercion
alies-dev Nov 10, 2026
fedcb7c
Merge branch 'fix/248-argument-coercion' into fix/249-type-alias-params
alies-dev Nov 10, 2026
aa8dfec
Merge remote-tracking branch 'origin/master' into fix/249-type-alias-…
alies-dev Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions src/Hooks/DataProviderNarrowingHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

use Psalm\Codebase;
use Psalm\Internal\Analyzer\MethodAnalyzer;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\TypeExpander;
use Psalm\PhpUnitPlugin\Dataset\DatasetBinding;
use Psalm\Plugin\EventHandler\BeforeStatementAnalysisInterface;
use Psalm\Plugin\EventHandler\Event\BeforeStatementAnalysisEvent;
Expand Down Expand Up @@ -205,16 +203,7 @@ private static function datasetType(Codebase $codebase, string $class, string $m
return null;
}

// A trait provider is a method of the class that uses it, so self is that class and not the trait
$appearing_id = $codebase->getAppearingMethodId($class . '::' . $method);
$self_class = $appearing_id === null ? $class : MethodIdentifier::wrap($appearing_id)->fq_class_name;
$return_type = TypeExpander::expandUnion(
$codebase,
$return_type,
$self_class,
$class,
$codebase->classlike_storage_provider->get($self_class)->parent_class,
);
$return_type = TestCaseHandler::expandForMethod($codebase, $return_type, $class . '::' . $method, $class);
if ($return_type->hasTemplate()) {
return null;
}
Expand Down
74 changes: 67 additions & 7 deletions src/Hooks/TestCaseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Internal\Type\TypeExpander;
use Psalm\IssueBuffer;
use Psalm\Issue;
use Psalm\PhpUnitPlugin\Dataset\DatasetBinding;
Expand All @@ -64,6 +65,7 @@
use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent;
use Psalm\StatementsSource;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Storage\MethodStorage;
use Psalm\Type;
use Psalm\Type\Atomic\TNull;
Expand Down Expand Up @@ -483,6 +485,13 @@ public static function afterStatementAnalysis(AfterClassLikeAnalysisEvent $event
continue;
}

$provider_return_type = self::expandForMethod(
$codebase,
$provider_return_type,
$apparent_provider_method_name,
$provider['class'] ?? $class_storage->name,
);

$provider_return_type_location = $codebase->getMethodReturnTypeLocation($provider_method_id);
assert(null !== $provider_return_type_location);
$provided_types[] = [
Expand Down Expand Up @@ -525,6 +534,13 @@ public static function afterStatementAnalysis(AfterClassLikeAnalysisEvent $event
];
}

$params = self::expandParams(
$codebase,
$method_storage->params,
$class_storage->name,
$class_storage->name . '::' . $method_name_lc,
);

foreach ($provided_types as $provided) {
$provider_name = $provided['name'];
$provider_return_type = $provided['type'];
Expand Down Expand Up @@ -718,13 +734,13 @@ static function (
if ($dataset_type instanceof Type\Atomic\TArray) {
// The dataset length is unknown, so assume the trailing params receive the return values
// of depended-upon tests, unless a variadic param could consume them.
$last_param = array_slice($method_storage->params, -1)[0] ?? null;
$last_param = array_slice($params, -1)[0] ?? null;
$provider_params = array_slice(
$method_storage->params,
$params,
0,
$last_param !== null && $last_param->is_variadic
? null
: max(0, count($method_storage->params) - $dependency_values),
: max(0, count($params) - $dependency_values),
);
// check that all of the required (?) params accept value type
$potential_argument_type = $dataset_type->type_params[1];
Expand All @@ -736,7 +752,7 @@ static function (
}
} elseif (self::hasStringKeys($dataset_type)) {
$provided_by = $provider_name . ':(' . $provider_return_type_string . ')';
$binding = DatasetBinding::bind($dataset_type, $method_storage->params, $codebase);
$binding = DatasetBinding::bind($dataset_type, $params, $codebase);

foreach ($binding->arguments as $argument) {
$checkParam(
Expand Down Expand Up @@ -800,14 +816,14 @@ static function (
// PHPUnit appends the return values of depended-upon tests right after the dataset, so the
// dataset certainly supplies only the positions that the required params need beyond them.
// A trailing variadic param could consume them as well.
$last_param = array_slice($method_storage->params, -1)[0] ?? null;
$tail_checked_until = count($method_storage->params);
$last_param = array_slice($params, -1)[0] ?? null;
$tail_checked_until = count($params);
if ($dependency_values > 0 && ($last_param === null || !$last_param->is_variadic)) {
$tail_checked_until = ($method_storage->required_param_count ?? $tail_checked_until)
- $dependency_values;
}

foreach ($method_storage->params as $param_offset => $param) {
foreach ($params as $param_offset => $param) {
$potential_argument_type = $potential_argument_types[$param_offset] ?? null;
if ($potential_argument_type === null && $list_tail !== null) {
if ($param->is_variadic) {
Expand Down Expand Up @@ -874,6 +890,50 @@ static function (
}
}

/**
* `self` is the class the method appears in (the class importing a trait's method), `static` the analysed one.
*
* @internal
*/
public static function expandForMethod(
Codebase $codebase,
Type\Union $type,
string $apparent_method_name,
string $static_class
): Type\Union {
$appearing_id = $codebase->getAppearingMethodId($apparent_method_name);
$self_class = $appearing_id === null ? $static_class : MethodIdentifier::wrap($appearing_id)->fq_class_name;

return TypeExpander::expandUnion(
$codebase,
$type,
$self_class,
$static_class,
$codebase->classlike_storage_provider->get($self_class)->parent_class,
);
}

/**
* @param list<FunctionLikeParameter> $params
* @return list<FunctionLikeParameter>
*/
private static function expandParams(
Codebase $codebase,
array $params,
string $static_class,
string $apparent_method_name
): array {
foreach ($params as $offset => $param) {
if ($param->type !== null) {
$params[$offset] = $param->setType(
self::expandForMethod($codebase, $param->type, $apparent_method_name, $static_class),
);
}
}

return $params;
}

/**
* Checks the providers of a test inherited from an ancestor that the class implements or overrides itself.
*
Expand Down
217 changes: 217 additions & 0 deletions tests/acceptance/Datasets.feature
Original file line number Diff line number Diff line change
Expand Up @@ -975,3 +975,220 @@ Feature: Datasets
"""
When I run Psalm
Then I see no errors

Scenario: An imported type alias in a test parameter is expanded in the provider check
Given I have the following code
"""
use PHPUnit\Framework\Attributes\DataProvider;

/** @psalm-type Options = array{host: string} */
final class Connection {}

/** @psalm-import-type Options from Connection */
final class MyTestCase extends TestCase {
/**
* @return list<array{array<string, string>}>
* @psalm-pure
*/
public static function provide(): array {
return [[['host' => 'x']]];
}

/**
* @param Options $options
*/
#[DataProvider('provide')]
public function testAlias(array $options): void {
$this->assertSame('x', $options['host']);
}

/**
* @param array{host: string} $options
*/
#[DataProvider('provide')]
public function testShape(array $options): void {
$this->assertSame('x', $options['host']);
}
}
"""
When I run Psalm
Then I see these errors
| Type | Message | Line |
| ArgumentTypeCoercion | Argument 1 of NS\MyTestCase::testAlias expects array{host: string}, but parent type array<string, string> provided by NS\MyTestCase::provide():(list<list{array<string, string>}>) | 23 |
| ArgumentTypeCoercion | Argument 1 of NS\MyTestCase::testShape expects array{host: string}, but parent type array<string, string> provided by NS\MyTestCase::provide():(list<list{array<string, string>}>) | 31 |
And I see no other errors

Scenario: An imported type alias in a test parameter accepts a matching dataset
Given I have the following code
"""
use PHPUnit\Framework\Attributes\DataProvider;

/** @psalm-type Options = array{host: string} */
final class Connection {}

/** @psalm-import-type Options from Connection */
final class MyTestCase extends TestCase {
/**
* @return list<array{array{host: string}}>
* @psalm-pure
*/
public static function provide(): array {
return [[['host' => 'x']]];
}

/**
* @param Options $options
*/
#[DataProvider('provide')]
public function testSomething(array $options): void {
$this->assertSame('x', $options['host']);
}
}
"""
When I run Psalm
Then I see no errors

Scenario: An imported type alias in a test parameter is expanded for named and unknown length datasets
Given I have the following code
"""
use PHPUnit\Framework\Attributes\DataProvider;

/** @psalm-type Options = array{host: string} */
final class Connection {}

/** @psalm-import-type Options from Connection */
final class MyTestCase extends TestCase {
/**
* @return list<array{options: array<string, string>}>
* @psalm-pure
*/
public static function named(): array {
return [['options' => ['host' => 'x']]];
}

/**
* @return list<array<int, array<string, string>>>
* @psalm-pure
*/
public static function lengthless(): array {
return [[['host' => 'x']]];
}

/**
* @param Options $options
*/
#[DataProvider('named')]
public function testNamed(array $options): void {
$this->assertSame('x', $options['host']);
}

/**
* @param Options $options
*/
#[DataProvider('lengthless')]
public function testLengthless(array $options): void {
$this->assertSame('x', $options['host']);
}
}
"""
When I run Psalm
Then I see these errors
| Type | Message | Line |
| ArgumentTypeCoercion | Argument 1 ($options) of NS\MyTestCase::testNamed expects array{host: string}, but parent type array<string, string> provided by NS\MyTestCase::named():(list<array{options: array<string, string>}>) | 31 |
| ArgumentTypeCoercion | Argument 1 of NS\MyTestCase::testLengthless expects array{host: string}, but parent type array<string, string> provided by NS\MyTestCase::lengthless():(list<array<int, array<string, string>>>) | 39 |
And I see no other errors

Scenario: A test parameter typed with an imported type alias is narrowed to the dataset type
Given I have the following code
"""
use PHPUnit\Framework\Attributes\DataProvider;

/** @psalm-type Options = array{host: string} */
final class Connection {}

/** @psalm-import-type Options from Connection */
final class MyTestCase extends TestCase {
/**
* @return list<array{array{host: 'x'}}>
* @psalm-pure
*/
public static function provide(): array {
return [[['host' => 'x']]];
}

/**
* @param Options $options
*/
#[DataProvider('provide')]
public function testSomething(array $options): void {
/** @psalm-check-type-exact $options = array{host: 'x'} */
$this->expectNotToPerformAssertions();
}
}
"""
When I run Psalm
Then I see no errors

Scenario: A variadic parameter typed with an imported type alias is expanded in the provider check
Given I have the following code
"""
use PHPUnit\Framework\Attributes\DataProvider;

/** @psalm-type Options = array{host: string} */
final class Connection {}

/** @psalm-import-type Options from Connection */
final class MyTestCase extends TestCase {
/**
* @return list<array{array<string, string>, array<string, string>}>
* @psalm-pure
*/
public static function provide(): array {
return [[['host' => 'x'], ['host' => 'y']]];
}

/**
* @param Options ...$options
*/
#[DataProvider('provide')]
public function testSomething(array ...$options): void {
$this->assertNotEmpty($options);
}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| ArgumentTypeCoercion | Argument 1 of NS\MyTestCase::testSomething expects array{host: string}, but parent type array<string, string> provided by NS\MyTestCase::provide():(list<list{array<string, string>, array<string, string>}>) |
| ArgumentTypeCoercion | Argument 2 of NS\MyTestCase::testSomething expects array{host: string}, but parent type array<string, string> provided by NS\MyTestCase::provide():(list<list{array<string, string>, array<string, string>}>) |
And I see no other errors

Scenario: self in a trait test and its provider means the class using the trait
Given I have the following code
"""
use PHPUnit\Framework\Attributes\DataProvider;

trait ProvidesSelf {
/**
* @return list<array{class-string<self>}>
* @psalm-pure
*/
public static function provide(): array {
return [[self::class]];
}

/**
* @param class-string<self> $class
*/
#[DataProvider('provide')]
public function testSomething(string $class): void {
$this->assertSame(self::class, $class);
}
}

final class MyTestCase extends TestCase {
use ProvidesSelf;
}
"""
When I run Psalm
Then I see no errors
Loading