Skip to content

Commit

Permalink
Merge pull request #45 from weirdan/external-data-providers
Browse files Browse the repository at this point in the history
External data providers
  • Loading branch information
weirdan authored Jan 10, 2020
2 parents a577f90 + 4784273 commit 021eb5a
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 8 deletions.
54 changes: 46 additions & 8 deletions hooks/TestCaseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,27 @@ public static function afterStatementAnalysis(
$provider_docblock_location = clone $method_storage->location;
$provider_docblock_location->setCommentLine($line);

$apparent_provider_method_name = $class_storage->name . '::' . (string) $provider;
if (false !== strpos($provider, '::')) {
[$class_name, $method_id] = explode('::', $provider);
$fq_class_name = Type::getFQCLNFromString($class_name, $statements_source->getAliases());

if (!$codebase->classOrInterfaceExists($fq_class_name, $provider_docblock_location)) {
IssueBuffer::accepts(new Issue\UndefinedClass(
'Class ' . $fq_class_name . ' does not exist',
$provider_docblock_location,
$fq_class_name
));
continue;
}
$apparent_provider_method_name = $fq_class_name . '::' . $method_id;
} else {
$apparent_provider_method_name = $class_storage->name . '::' . (string) $provider;
}

$apparent_provider_method_name = preg_replace('/\(\s*\)$/', '', $apparent_provider_method_name);

$provider_method_id = $codebase->getDeclaringMethodId($apparent_provider_method_name);

// methodExists also can mark methods as used (weird, but handy)
if (null === $provider_method_id) {
IssueBuffer::accepts(new Issue\UndefinedMethod(
'Provider method ' . $apparent_provider_method_name . ' is not defined',
Expand All @@ -158,6 +175,7 @@ public static function afterStatementAnalysis(
continue;
}

// methodExists also can mark methods as used (weird, but handy)
$provider_method_exists = $codebase->methodExists(
$provider_method_id,
$provider_docblock_location,
Expand Down Expand Up @@ -189,7 +207,7 @@ public static function afterStatementAnalysis(
Type::getArray(),
]);

foreach ($provider_return_type->getTypes() as $type) {
foreach (self::getAtomics($provider_return_type) as $type) {
if (!$type->isIterable($codebase)) {
IssueBuffer::accepts(new Issue\InvalidReturnType(
'Providers must return ' . $expected_provider_return_type->getId()
Expand Down Expand Up @@ -301,8 +319,8 @@ function (
}
};

/** @var Type\Atomic\TArray|Type\Atomic\ObjectLike $dataset_type */
$dataset_type = $provider_return_type->type_params[1]->getTypes()['array'];
/** @var Type\Atomic\TArray|Type\Atomic\ObjectLike|Type\Atomic\TList $dataset_type */
$dataset_type = self::getAtomics($provider_return_type->type_params[1])['array'];

if ($dataset_type instanceof Type\Atomic\TArray) {
// check that all of the required (?) params accept value type
Expand All @@ -313,7 +331,15 @@ function (
}
$checkParam($potential_argument_type, $param->type, $param->is_optional, $param_offset);
}
} else {
} elseif ($dataset_type instanceof Type\Atomic\TList) {
$potential_argument_type = $dataset_type->type_param;
foreach ($method_storage->params as $param_offset => $param) {
if (!$param->type) {
continue;
}
$checkParam($potential_argument_type, $param->type, $param->is_optional, $param_offset);
}
} else { // ObjectLike
// iterate over all params checking if corresponding value type is acceptable
// let's hope properties are sorted in array order
$potential_argument_types = array_values($dataset_type->properties);
Expand Down Expand Up @@ -346,7 +372,7 @@ function (

assert(null !== $param->type);
if ($param->is_variadic) {
$param_types = $param->type->getTypes();
$param_types = self::getAtomics($param->type);
$variadic_param_type = new Type\Union(array_values($param_types));

// check remaining argument types
Expand All @@ -369,6 +395,18 @@ function (
}
}

/** @return Type\Atomic[] */
private static function getAtomics(Type\Union $union): array
{
if (method_exists($union, 'getAtomicTypes')) {
/** @var Type\Atomic[] annotated for versions missing the method */
return $union->getAtomicTypes();
} else {
/** @psalm-suppress DeprecatedMethod annotated for newer versions that deprecated the method */
return $union->getTypes();
}
}

private static function unionizeIterables(Codebase $codebase, Type\Union $iterables): Type\Atomic\TIterable
{
/** @var Type\Union[] $key_types */
Expand All @@ -377,7 +415,7 @@ private static function unionizeIterables(Codebase $codebase, Type\Union $iterab
/** @var Type\Union[] $value_types */
$value_types = [];

foreach ($iterables->getTypes() as $type) {
foreach (self::getAtomics($iterables) as $type) {
if (!$type->isIterable($codebase)) {
throw new \RuntimeException('should be iterable');
}
Expand Down
147 changes: 147 additions & 0 deletions tests/acceptance/TestCase.feature
Original file line number Diff line number Diff line change
Expand Up @@ -1062,3 +1062,150 @@ Feature: TestCase
"""
When I run Psalm with dead code detection
Then I see no errors


@ExternalProviders
Scenario: External providers are allowed
Given I have the following code
"""
class External {
/** @return iterable<string, array<int,int>> */
public function provide(): iterable {
yield "dataset name" => [1];
}
}
class MyTestCase extends TestCase {
/** @dataProvider External::provide */
public function testSomething(int $_p): void {}
}
"""
When I run Psalm
Then I see no errors

@ExternalProviders
Scenario: External providers with parens are allowed
Given I have the following code
"""
class External {
/** @return iterable<string, array<int,int>> */
public function provide(): iterable {
yield "dataset name" => [1];
}
}
class MyTestCase extends TestCase {
/** @dataProvider External::provide() */
public function testSomething(int $_p): void {}
}
"""
When I run Psalm
Then I see no errors

@ExternalProviders
Scenario: External fully qualified providers are allowed
Given I have the following code
"""
class External {
/** @return iterable<string, array<int,int>> */
public function provide(): iterable {
yield "dataset name" => [1];
}
}
class MyTestCase extends TestCase {
/** @dataProvider \NS\External::provide */
public function testSomething(int $_p): void {}
}
"""
When I run Psalm
Then I see no errors

@ExternalProviders
Scenario: Missing external provider classes are reported
Given I have the following code
"""
class MyTestCase extends TestCase {
/** @dataProvider External::provide */
public function testSomething(int $_p): void {}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| UndefinedClass | Class NS\External does not exist |


@ExternalProviders
Scenario: External providers are not marked as unused
Given I have the following code
"""
class External {
/** @return iterable<string, array<int,int>> */
public function provide(): iterable {
yield "dataset name" => [1];
}
}
class MyTestCase extends TestCase {
/** @dataProvider External::provide */
public function testSomething(int $_p): void {}
}
"""
When I run Psalm with dead code detection
Then I see no errors

@ExternalProviders
Scenario: Mismatched external providers are reported
Given I have the following code
"""
class External {
/** @return iterable<string, array<int,string>> */
public function provide(): iterable {
yield "dataset name" => ["1"];
}
}
class MyTestCase extends TestCase {
/** @dataProvider External::provide */
public function testSomething(int $_p): void {}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| InvalidArgument | Argument 1 of NS\MyTestCase::testSomething expects int, string provided by NS\External::provide():(iterable<string, array<int, string>>) |

@List
Scenario: Providers returning list are ok
Given I have the following code
"""
class MyTestCase extends TestCase {
/** @return iterable<string, list<int>> */
public function provide(): iterable {
yield "dataset name" => [1];
}
/** @dataProvider provide */
public function testSomething(int $_p): void {}
}
"""
When I run Psalm
Then I see no errors

@List
Scenario: Providers returning mismatching list are reported
Given I have the following code
"""
class MyTestCase extends TestCase {
/** @return iterable<string, list<string>> */
public function provide(): iterable {
yield "dataset name" => ["1"];
}
/** @dataProvider provide */
public function testSomething(int $_p): void {}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| InvalidArgument | Argument 1 of NS\MyTestCase::testSomething expects int, string provided by NS\MyTestCase::provide():(iterable<string, list<string>>) |

0 comments on commit 021eb5a

Please sign in to comment.