diff --git a/.travis.yml b/.travis.yml index 5cac828..1bd1568 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,9 @@ before_install: - composer clear-cache install: - - composer install + - travis_retry composer install - if [[ "$DEPS" = 'high' || "$DEPS" = 'dev' ]]; then travis_retry composer $DEFAULT_COMPOSER_FLAGS update; fi - - if [[ "$DEPS" = 'high' ]]; then composer require $DEFAULT_COMPOSER_FLAGS --prefer-stable vimeo/psalm; fi + - if [[ "$DEPS" = 'high' ]]; then composer require $DEFAULT_COMPOSER_FLAGS --prefer-stable --update-with-dependencies vimeo/psalm; fi - if [[ "$DEPS" = 'low' ]]; then travis_retry composer $DEFAULT_COMPOSER_FLAGS --prefer-lowest --prefer-stable update; fi - if [[ "$DEPS" = 'stable' ]]; then travis_retry composer $DEFAULT_COMPOSER_FLAGS --prefer-stable update; fi - ./vendor/bin/psalm --version diff --git a/composer.json b/composer.json index 44fe6ef..c024582 100755 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require-dev": { "squizlabs/php_codesniffer": "^3.3.1", "codeception/base": "^2.5", - "weirdan/codeception-psalm-module": "^0.2.2" + "weirdan/codeception-psalm-module": "^0.4.0" }, "extra": { "psalm": { diff --git a/hooks/TestCaseHandler.php b/hooks/TestCaseHandler.php index bda0441..63f1838 100644 --- a/hooks/TestCaseHandler.php +++ b/hooks/TestCaseHandler.php @@ -7,11 +7,11 @@ use PHPUnit\Framework\TestCase; use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\ClassMethod; -use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\DocComment; use Psalm\Exception\DocblockParseException; use Psalm\FileSource; +use Psalm\Internal\PhpVisitor\ReflectorVisitor; use Psalm\IssueBuffer; use Psalm\Issue; use Psalm\Plugin\Hook\AfterClassLikeAnalysisInterface; @@ -76,6 +76,24 @@ public static function afterClassLikeVisit( if (self::hasInitializers($class_storage, $class_node)) { $class_storage->custom_metadata[__NAMESPACE__] = ['hasInitializers' => true]; } + + $file_path = $statements_source->getFilePath(); + $file_storage = $codebase->file_storage_provider->get($file_path); + + foreach ($class_node->getMethods() as $method) { + $specials = self::getSpecials($method); + if (!isset($specials['dataProvider'])) { + continue; + } + foreach ($specials['dataProvider'] as $provider) { + if (false !== strpos($provider, '::')) { + [$class_name] = explode('::', $provider); + $fq_class_name = Type::getFQCLNFromString($class_name, $statements_source->getAliases()); + $codebase->scanner->queueClassLikeForScanning($fq_class_name, $file_path); + $file_storage->referenced_classlikes[strtolower($fq_class_name)] = $fq_class_name; + } + } + } } /** @@ -132,7 +150,7 @@ public static function afterStatementAnalysis( } $codebase->methodExists( - $declaring_method_id, + (string) $declaring_method_id, null, 'PHPUnit\Framework\TestSuite::run' ); @@ -177,9 +195,9 @@ public static function afterStatementAnalysis( // methodExists also can mark methods as used (weird, but handy) $provider_method_exists = $codebase->methodExists( - $provider_method_id, + (string) $provider_method_id, $provider_docblock_location, - $declaring_method_id + (string) $declaring_method_id ); if (!$provider_method_exists) { @@ -395,15 +413,17 @@ function ( } } - /** @return Type\Atomic[] */ + /** @return non-empty-array */ private static function getAtomics(Type\Union $union): array { if (method_exists($union, 'getAtomicTypes')) { - /** @var Type\Atomic[] annotated for versions missing the method */ + /** @var non-empty-array annotated for versions missing the method */ return $union->getAtomicTypes(); } else { /** @psalm-suppress DeprecatedMethod annotated for newer versions that deprecated the method */ - return $union->getTypes(); + $types = $union->getTypes(); + assert(!empty($types)); + return $types; } } @@ -436,13 +456,6 @@ private static function unionizeIterables(Codebase $codebase, Type\Union $iterab } } - if (empty($key_types) || empty($value_types)) { - return new Type\Atomic\TIterable([ - Type::getMixed(), - Type::getMixed(), - ]); - } - $combine = /** @param null|Type\Union $a */ function ($a, Type\Union $b) use ($codebase): Type\Union { diff --git a/tests/acceptance/Assert75.feature b/tests/acceptance/Assert75.feature index 9a1b7ef..aed8fb0 100644 --- a/tests/acceptance/Assert75.feature +++ b/tests/acceptance/Assert75.feature @@ -127,24 +127,17 @@ Feature: Assert (PHPUnit 7.5+) Scenario: Assert::assertIsScalar() Given I have the following code """ - /** @psalm-suppress MixedAssignment */ - $s = mixed(); + /** @return scalar */ + function f() { + /** @psalm-suppress MixedAssignment */ + $s = mixed(); - Assert::assertIsScalar($s); // int|string|float|bool - // all of the following should cause errors - if (is_array($s)) {} - if (is_resource($s)) {} - if (is_object($s)) {} - if (is_null($s)) {} + Assert::assertIsScalar($s); + return $s; + } """ When I run Psalm - Then I see these errors - | Type | Message | - | DocblockTypeContradiction | Found a contradiction with a docblock-defined type when evaluating $s and trying to reconcile type 'scalar' to array | - | DocblockTypeContradiction | Cannot resolve types for $s - docblock-defined type scalar does not contain resource | - | DocblockTypeContradiction | Found a contradiction with a docblock-defined type when evaluating $s and trying to reconcile type 'scalar' to object | - | DocblockTypeContradiction | Cannot resolve types for $s - docblock-defined type scalar does not contain null | - And I see no other errors + Then I see no errors Scenario: Assert::assertIsCallable() Given I have the following code diff --git a/tests/acceptance/TestCase.feature b/tests/acceptance/TestCase.feature index 6a9a59b..6428f92 100644 --- a/tests/acceptance/TestCase.feature +++ b/tests/acceptance/TestCase.feature @@ -7,7 +7,7 @@ Feature: TestCase Given I have the following config """ - + @@ -1176,6 +1176,38 @@ Feature: TestCase | Type | Message | | InvalidArgument | Argument 1 of NS\MyTestCase::testSomething expects int, string provided by NS\External::provide():(iterable>) | + @ExternalProviders + Scenario: External providers are available even when Psalm is asked to analyze single test case + Given I have the following code in "test.php" + """ + > */ + public function provide(): iterable { + yield "dataset name" => [1]; + } + } + """ + And I have the following classmap + | Class | File | + | NS\MyTestCase | test.php | + | NS\External | ext.php | + When I run Psalm on "test.php" + Then I see no errors + @List Scenario: Providers returning list are ok Given I have the following code