Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented support for #[Test], #[DataProvider], #[Before] attributes, cleaned up old PHPUnit/Prophecy support #149

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 43 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ jobs:
- name: Run PHPCodeSniffer
run: vendor/bin/phpcs --report=checkstyle -q --parallel=1 | cs2pr

test-static-analysis:
name: Run static analysis on the tests themselves
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
tools: composer:v2
coverage: none
extensions: intl, mbstring, bcmath, sodium
env:
fail-fast: true

- name: Install composer dependencies (high deps)
run: cd tools/behat && composer install

- name: Static analysis
run: cd tools/behat && vendor/bin/psalm

tests:
name: Test on ${{matrix.php}} - ${{matrix.deps}} deps
runs-on: ubuntu-20.04
Expand Down Expand Up @@ -135,20 +159,38 @@ jobs:
env:
COMPOSER_ROOT_VERSION: dev-master

- name: Install composer tool dependencies (high deps)
run: cd tools/behat && composer update --prefer-dist --no-interaction
if: ${{matrix.deps == 'high'}}
env:
COMPOSER_ROOT_VERSION: dev-master

- name: Install composer dependencies (low deps)
run: composer update --prefer-dist --no-interaction --prefer-stable --prefer-lowest
if: ${{matrix.deps == 'low'}}
env:
COMPOSER_ROOT_VERSION: dev-master

- name: Install composer tool dependencies (low deps)
run: cd tools/behat && composer update --prefer-dist --no-interaction --prefer-stable --prefer-lowest
if: ${{matrix.deps == 'low'}}
env:
COMPOSER_ROOT_VERSION: dev-master

- name: Install composer dependencies (stable deps)
run: composer update --prefer-dist --no-interaction --prefer-stable
if: ${{matrix.deps == 'stable'}}
env:
COMPOSER_ROOT_VERSION: dev-master

- name: Install composer tool dependencies (stable deps)
run: cd tools/behat && composer update --prefer-dist --no-interaction --prefer-stable
if: ${{matrix.deps == 'stable'}}
env:
COMPOSER_ROOT_VERSION: dev-master

- name: Show Psalm version
run: vendor/bin/psalm --version

- name: Run tests
run: vendor/bin/codecept run -v
run: cd tools/behat && vendor/bin/behat -vvv
10 changes: 4 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,17 @@
"require": {
"php": ">=8.1",
"ext-simplexml": "*",
"composer/semver": "^1.4 || ^2.0 || ^3.0",
"composer/package-versions-deprecated": "^1.10",
"vimeo/psalm": "dev-master || ^6"
},
"conflict": {
"phpunit/phpunit": "<7.5"
"phpunit/phpunit": "<8.5.1",
"phpspec/prophecy": "<1.20.0",
"phpspec/prophecy-phpunit": "<2.3.0"
},
"require-dev": {
"php": "^7.3 || ^8.0",
"codeception/codeception": "^4.0.3",
"phpunit/phpunit": "^7.5 || ^8.0 || ^9.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"squizlabs/php_codesniffer": "^3.3.1",
"weirdan/codeception-psalm-module": "^0.11.0",
"weirdan/prophecy-shim": "^1.0 || ^2.0"
},
"extra": {
Expand Down
1 change: 1 addition & 0 deletions psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedPsalmSuppress="true"
>
<projectFiles>
<directory name="src" />
Expand Down
114 changes: 91 additions & 23 deletions src/Hooks/TestCaseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@

use Error;
use PhpParser\Comment\Doc;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Psalm\Aliases;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\DocComment;
use Psalm\Exception\DocblockParseException;
use Psalm\IssueBuffer;
use Psalm\Issue;
use Psalm\PhpUnitPlugin\VersionUtils;
use Psalm\Plugin\EventHandler\AfterClassLikeAnalysisInterface;
use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface;
use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface;
Expand All @@ -27,6 +34,12 @@
use Psalm\Type\Union;
use RuntimeException;

use function array_column;
use function array_filter;
use function array_map;
use function array_merge;
use function array_values;

class TestCaseHandler implements
AfterClassLikeVisitInterface,
AfterClassLikeAnalysisInterface,
Expand Down Expand Up @@ -76,20 +89,21 @@ private static function getDescendants(Codebase $codebase, string $name): array
*/
public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event)
{
$class_node = $event->getStmt();
$class_storage = $event->getStorage();
$class_node = $event->getStmt();
$class_storage = $event->getStorage();
$statements_source = $event->getStatementsSource();
$codebase = $event->getCodebase();
$codebase = $event->getCodebase();
$aliases = $statements_source->getAliases();

if (self::hasInitializers($class_storage, $class_node)) {
if (self::hasInitializers($class_storage, $class_node, $aliases)) {
$class_storage->custom_metadata[__NAMESPACE__] = ['hasInitializers' => true];
}

$file_path = $statements_source->getFilePath();
$file_path = $statements_source->getFilePath();
$file_storage = $codebase->file_storage_provider->get($file_path);

foreach ($class_node->getMethods() as $method) {
$specials = self::getSpecials($method);
$specials = self::getSpecials($method, $aliases);
if (!isset($specials['dataProvider'])) {
continue;
}
Expand All @@ -109,10 +123,11 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event)
*/
public static function afterStatementAnalysis(AfterClassLikeAnalysisEvent $event)
{
$class_node = $event->getStmt();
$class_storage = $event->getClasslikeStorage();
$codebase = $event->getCodebase();
$class_node = $event->getStmt();
$class_storage = $event->getClasslikeStorage();
$codebase = $event->getCodebase();
$statements_source = $event->getStatementsSource();
$aliases = $statements_source->getAliases();

if (!$codebase->classExtends($class_storage->name, 'PHPUnit\Framework\TestCase')) {
return null;
Expand All @@ -130,9 +145,9 @@ public static function afterStatementAnalysis(AfterClassLikeAnalysisEvent $event
}

foreach ($class_storage->declaring_method_ids as $method_name_lc => $declaring_method_id) {
$method_name = $codebase->getCasedMethodId($class_storage->name . '::' . $method_name_lc);
$method_name = $codebase->getCasedMethodId($class_storage->name . '::' . $method_name_lc);
$method_storage = $codebase->methods->getStorage($declaring_method_id);
[$declaring_method_class, $declaring_method_name] = explode('::', (string) $declaring_method_id);
[$declaring_method_class, $declaring_method_name] = explode('::', (string)$declaring_method_id);
$declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_method_class);

$declaring_class_node = $class_node;
Expand All @@ -150,15 +165,15 @@ public static function afterStatementAnalysis(AfterClassLikeAnalysisEvent $event
continue;
}

$specials = self::getSpecials($stmt_method);
$specials = self::getSpecials($stmt_method, $aliases);

$is_test = 0 === strpos($method_name_lc, 'test') || isset($specials['test']);
if (!$is_test) {
continue; // skip non-test methods
}

$codebase->methodExists(
(string) $declaring_method_id,
(string)$declaring_method_id,
null,
'PHPUnit\Framework\TestSuite::run'
);
Expand Down Expand Up @@ -476,10 +491,10 @@ private static function unionizeIterables(Codebase $codebase, Type\Union $iterab
}

if ($type instanceof Type\Atomic\TArray) {
$key_types[] = $type->type_params[0] ?? Type::getMixed();
$key_types[] = $type->type_params[0] ?? Type::getMixed();
$value_types[] = $type->type_params[1] ?? Type::getMixed();
} elseif ($type instanceof Type\Atomic\TKeyedArray) {
$key_types[] = $type->getGenericKeyType();
$key_types[] = $type->getGenericKeyType();
$value_types[] = $type->getGenericValueType();
} elseif ($type instanceof Type\Atomic\TNamedObject || $type instanceof Type\Atomic\TIterable) {
[$key_types[], $value_types[]] = $codebase->getKeyValueParamsForTraversableObject($type);
Expand All @@ -501,7 +516,7 @@ static function ($a, Type\Union $b) use ($codebase): Type\Union {
}


private static function hasInitializers(ClassLikeStorage $storage, ClassLike $stmt): bool
private static function hasInitializers(ClassLikeStorage $storage, ClassLike $stmt, Aliases $aliases): bool
{
if (isset($storage->methods['setup'])) {
return true;
Expand All @@ -512,21 +527,75 @@ private static function hasInitializers(ClassLikeStorage $storage, ClassLike $st
if (!$stmt_method) {
continue;
}
if (self::isBeforeInitializer($stmt_method)) {
if (self::isBeforeInitializer($stmt_method, $aliases)) {
return true;
}
}
return false;
}

private static function isBeforeInitializer(ClassMethod $method): bool
private static function isBeforeInitializer(ClassMethod $method, Aliases $aliases): bool
{
return isset(self::getSpecials($method, $aliases)['before']);
}

/** @return array<string, array<int,string>> */
private static function getSpecials(ClassMethod $method, Aliases $aliases): array
{
return array_merge(
self::getDocblockSpecials($method),
// Attributes take priority over docblocks
self::getAttributeSpecials($method, $aliases),
);
}

/**
* @template T of object
* @param class-string<T> $attributeClass
* @return array<int, string>|null
*/
private static function attributeValue(ClassMethod $method, Aliases $aliases, string $attributeClass): array|null
{
return array_values(array_merge(
...array_map(
static fn(AttributeGroup $group): array => array_map(
static function (Attribute $attribute): array|null {
// For our purposes, we only care about string literals: everything else is currently out of scope.
// If you need more complex expressions supported, add a constant expression evaluator here.
return array_map(
static fn(String_ $string): string => $string->value,
array_filter(
array_column($attribute->args, 'value'),
static fn(Expr $expression): bool => $expression instanceof String_,
)
);
},
array_filter(
$group->attrs,
static fn(Attribute $attribute): bool
=> $attributeClass === Type::getFQCLNFromString(
$attribute->name->toString(),
$aliases,
),
),
),
array_values($method->getAttrGroups()),
),
))[0] ?? null;
}

/** @return array<string, array<int,string>> */
private static function getAttributeSpecials(ClassMethod $method, Aliases $aliases): array
{
$specials = self::getSpecials($method);
return isset($specials['before']);
return array_filter([
'before' => self::attributeValue($method, $aliases, Before::class),
'test' => self::attributeValue($method, $aliases, Test::class),
'dataProvider' => self::attributeValue($method, $aliases, DataProvider::class),
], static fn(array|null $special): bool => $special !== null);
}

/** @return array<string, array<int,string>> */
private static function getSpecials(ClassMethod $method): array
private static function getDocblockSpecials(ClassMethod $method): array
{
$docblock = $method->getDocComment();
if (!$docblock) {
Expand Down Expand Up @@ -574,7 +643,6 @@ private static function queueClassLikeForScanning(
$codebase->queueClassLikeForScanning($fq_class_name);
} else {
/**
* @psalm-suppress InvalidScalarArgument
* @psalm-suppress InvalidArgument
*/
$codebase->scanner->queueClassLikeForScanning($fq_class_name, $file_path);
Expand Down
6 changes: 0 additions & 6 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@ class Plugin implements PluginEntryPointInterface
/** @return void */
public function __invoke(RegistrationInterface $psalm, ?SimpleXMLElement $config = null): void
{
if (VersionUtils::packageVersionIs('phpunit/phpunit', '<', '8.0')) {
$psalm->addStubFile(__DIR__ . '/../stubs/Assert_75.phpstub');
}
$psalm->addStubFile(__DIR__ . '/../stubs/TestCase.phpstub');
$psalm->addStubFile(__DIR__ . '/../stubs/MockBuilder.phpstub');
$psalm->addStubFile(__DIR__ . '/../stubs/InvocationMocker.phpstub');
$psalm->addStubFile(__DIR__ . '/../stubs/Prophecy.phpstub');

class_exists(Hooks\TestCaseHandler::class, true);
$psalm->registerHooksFromClass(Hooks\TestCaseHandler::class);
Expand Down
34 changes: 0 additions & 34 deletions src/VersionUtils.php

This file was deleted.

Loading