Skip to content
Merged
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
19 changes: 7 additions & 12 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +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 || ^7"
"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",
"behat/gherkin": "~4.11.0",
"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 All @@ -47,13 +44,11 @@
"scripts": {
"check": [
"@cs-check",
"@analyze",
"@test"
"@analyze"
],
"analyze": "psalm",
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "codecept run -v"
"cs-fix": "phpcbf"
},
"config": {
"optimize-autoloader": true,
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
112 changes: 89 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,7 +34,13 @@
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

Check failure on line 43 in src/Hooks/TestCaseHandler.php

View workflow job for this annotation

GitHub Actions / Static analysis

ClassMustBeFinal

src/Hooks/TestCaseHandler.php:43:7: ClassMustBeFinal: Class Psalm\PhpUnitPlugin\Hooks\TestCaseHandler is never extended and is not part of the public API, and thus must be made final. (see https://psalm.dev/361)
AfterClassLikeVisitInterface,
AfterClassLikeAnalysisInterface,
AfterCodebasePopulatedInterface
Expand All @@ -35,7 +48,7 @@
/**
* {@inheritDoc}
*/
public static function afterCodebasePopulated(AfterCodebasePopulatedEvent $event)

Check failure on line 51 in src/Hooks/TestCaseHandler.php

View workflow job for this annotation

GitHub Actions / Static analysis

MissingOverrideAttribute

src/Hooks/TestCaseHandler.php:51:5: MissingOverrideAttribute: Method Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::aftercodebasepopulated should have the "Override" attribute (see https://psalm.dev/358)
{
$codebase = $event->getCodebase();

Expand Down Expand Up @@ -74,22 +87,23 @@
/**
* {@inheritDoc}
*/
public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event)

Check failure on line 90 in src/Hooks/TestCaseHandler.php

View workflow job for this annotation

GitHub Actions / Static analysis

MissingOverrideAttribute

src/Hooks/TestCaseHandler.php:90:5: MissingOverrideAttribute: Method Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::afterclasslikevisit should have the "Override" attribute (see https://psalm.dev/358)
{
$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 @@ -107,12 +121,13 @@
/**
* {@inheritDoc}
*/
public static function afterStatementAnalysis(AfterClassLikeAnalysisEvent $event)

Check failure on line 124 in src/Hooks/TestCaseHandler.php

View workflow job for this annotation

GitHub Actions / Static analysis

MissingOverrideAttribute

src/Hooks/TestCaseHandler.php:124:5: MissingOverrideAttribute: Method Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::afterstatementanalysis should have the "Override" attribute (see https://psalm.dev/358)
{
$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 @@
}

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 @@
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 @@
}

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 @@
}


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,73 @@
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
{
$onlyStringLiteralExpressions = static fn (Attribute $attribute): array => array_map(
static fn(String_ $string): string => $string->value,
array_filter(
array_column($attribute->args, 'value'),
// 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.
static fn(Expr $expression): bool => $expression instanceof String_,
),
);
$attributesInGroupMatchingRequestedAttributeName = static fn(AttributeGroup $group): array => array_filter(
$group->attrs,
static fn(Attribute $attribute): bool => $attributeClass === Type::getFQCLNFromString(
$attribute->name->toString(),
$aliases,
),
);

foreach ($method->getAttrGroups() as $group) {
foreach ($attributesInGroupMatchingRequestedAttributeName($group) as $attribute) {
return $onlyStringLiteralExpressions($attribute);
}
}

return 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 +641,6 @@
$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 @@ -7,18 +7,12 @@
use Psalm\Plugin\RegistrationInterface;

/** @psalm-suppress UnusedClass */
class Plugin implements PluginEntryPointInterface

Check failure on line 10 in src/Plugin.php

View workflow job for this annotation

GitHub Actions / Static analysis

ClassMustBeFinal

src/Plugin.php:10:7: ClassMustBeFinal: Class Psalm\PhpUnitPlugin\Plugin is never extended and is not part of the public API, and thus must be made final. (see https://psalm.dev/361)
{
/** @return void */
public function __invoke(RegistrationInterface $psalm, ?SimpleXMLElement $config = null): void

Check failure on line 13 in src/Plugin.php

View workflow job for this annotation

GitHub Actions / Static analysis

MissingOverrideAttribute

src/Plugin.php:13:5: MissingOverrideAttribute: Method Psalm\PhpUnitPlugin\Plugin::__invoke should have the "Override" attribute (see https://psalm.dev/358)
{
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
Loading