diff --git a/.github/workflows/backend-ci.yaml b/.github/workflows/backend-ci.yaml new file mode 100644 index 0000000..750cdb6 --- /dev/null +++ b/.github/workflows/backend-ci.yaml @@ -0,0 +1,71 @@ +name: Backend build + +on: + push: + branches: + - main + - '[0-9]+.[0-9]+' + pull_request: ~ + +jobs: + cs-fix: + name: Run code style check + runs-on: "ubuntu-22.04" + strategy: + matrix: + php: + - '8.1' + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP Action + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: 'pdo_sqlite, gd' + tools: cs2pr + + - uses: ramsey/composer-install@v3 + with: + dependency-versions: "highest" + + - name: Run code style check + run: composer run-script check-cs -- --format=checkstyle | cs2pr + + tests: + name: Tests + runs-on: "ubuntu-22.04" + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.3' + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP Action + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: pdo_sqlite, gd + tools: cs2pr + + - uses: ramsey/composer-install@v3 + with: + dependency-versions: "highest" + composer-options: "--prefer-dist --no-progress --no-suggest" + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run PHPStan analysis + run: composer run-script phpstan + + - name: Run test suite + run: composer run-script --timeout=600 test diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..106e22d --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,14 @@ +setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__ . '/rules') + ->in(__DIR__ . '/tests') + ->files()->name('*.php') +); diff --git a/composer.json b/composer.json index fb38ac0..007907c 100644 --- a/composer.json +++ b/composer.json @@ -1,21 +1,54 @@ { - "name": "ibexa/phpstan", - "license": "proprietary", - "type": "ibexa-bundle", - "keywords": [ - "ibexa-dxp" - ], - "require": { - "php": "^7.4 || ^8.0" - }, - "config": { - "sort-packages": true - }, - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] + "name": "ibexa/phpstan", + "license": "proprietary", + "type": "ibexa-bundle", + "keywords": [ + "ibexa-dxp" + ], + "require": { + "php": "^7.4 || ^8.0", + "ibexa/core": "4.6.x-dev", + "ibexa/doctrine-schema": "4.6.x-dev" + }, + "require-dev": { + "ibexa/code-style": "~2.0.0", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.6", + "phpstan/phpstan-symfony": "^1.3", + "phpunit/phpunit": "^9" + }, + "autoload": { + "psr-4": { + "Ibexa\\PHPStan\\Rules\\": "rules/" + } + }, + "autoload-dev": { + "psr-4": { + "Ibexa\\Tests\\PHPStan\\Rules\\": "tests/rules/" + } + }, + "scripts": { + "fix-cs": "php-cs-fixer fix --config=.php-cs-fixer.php --show-progress=dots", + "check-cs": "@fix-cs --dry-run", + "test": "phpunit -c phpunit.xml.dist", + "phpstan": "phpstan analyse -c phpstan.neon" + }, + "scripts-descriptions": { + "fix-cs": "Automatically fixes code style in all files", + "check-cs": "Run code style checker for all files", + "test": "Run automatic tests", + "phpstan": "Run static code analysis" + }, + "config": { + "sort-packages": true, + "allow-plugins": false + }, + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } } - } } diff --git a/extension.neon b/extension.neon index bd6889e..80cf7ed 100644 --- a/extension.neon +++ b/extension.neon @@ -5,3 +5,5 @@ parameters: - stubs/Money/Money.stub - stubs/Money/MoneyFormatter.stub - stubs/Money/MoneyParser.stub +rules: + - Ibexa\PHPStan\Rules\NoConfigResolverParametersInConstructorRule diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..ab3d432 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: 8 + paths: + - rules + - tests + checkMissingCallableSignature: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..06983f7 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,12 @@ + + + + tests/rules + + + diff --git a/rules/NoConfigResolverParametersInConstructorRule.php b/rules/NoConfigResolverParametersInConstructorRule.php new file mode 100644 index 0000000..6bd3213 --- /dev/null +++ b/rules/NoConfigResolverParametersInConstructorRule.php @@ -0,0 +1,68 @@ + + */ +final class NoConfigResolverParametersInConstructorRule implements Rule +{ + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + /** + * @throws \PHPStan\ShouldNotHappenException + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + + $function = $scope->getFunction(); + if ($function !== null && $function->getName() !== '__construct') { + return []; + } + + /** @var \PhpParser\Node\Identifier $nodeName */ + $nodeName = $node->name; + $methodName = $nodeName->name; + + if ( + $methodName !== 'getParameter' + && $methodName !== 'hasParameter' + && !isset($node->getArgs()[0]) + ) { + return []; + } + + $type = $scope->getType($node->var); + $configResolverInterfaceType = new ObjectType(ConfigResolverInterface::class); + if (!$configResolverInterfaceType->isSuperTypeOf($type)->yes()) { + return []; + } + + return [ + RuleErrorBuilder + ::message('Referring to ConfigResolver parameters in constructor is not allowed due to potential scope change.') + ->identifier('Ibexa.NoConfigResolverParametersInConstructor') + ->nonIgnorable() + ->build(), + ]; + } +} diff --git a/tests/rules/Fixtures/NoConfigResolverParametersInConstructorFixture.php b/tests/rules/Fixtures/NoConfigResolverParametersInConstructorFixture.php new file mode 100644 index 0000000..f35396c --- /dev/null +++ b/tests/rules/Fixtures/NoConfigResolverParametersInConstructorFixture.php @@ -0,0 +1,30 @@ +configResolver = $configResolver; + + $configResolver->hasParameter('foo'); + $configResolver->getParameter('foo'); + } + + public function foo(): void + { + //this is allowed outside of constructor - no error reported by PHPStan + $this->configResolver->hasParameter('bar'); + } +} diff --git a/tests/rules/NoConfigResolverParametersInConstructorRuleTest.php b/tests/rules/NoConfigResolverParametersInConstructorRuleTest.php new file mode 100644 index 0000000..6af861b --- /dev/null +++ b/tests/rules/NoConfigResolverParametersInConstructorRuleTest.php @@ -0,0 +1,43 @@ + + */ +final class NoConfigResolverParametersInConstructorRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new NoConfigResolverParametersInConstructorRule(); + } + + public function testRule(): void + { + $this->analyse( + [ + __DIR__ . '/Fixtures/NoConfigResolverParametersInConstructorFixture.php', + ], + [ + [ + 'Referring to ConfigResolver parameters in constructor is not allowed due to potential scope change.', + 21, + ], + [ + 'Referring to ConfigResolver parameters in constructor is not allowed due to potential scope change.', + 22, + ], + ] + ); + } +}