diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f7ee48d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + open-pull-requests-limit: 10 + commit-message: + prefix: "ci" + include: "scope" + groups: + github-actions: + patterns: ["*"] + + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + open-pull-requests-limit: 10 + versioning-strategy: "increase-if-necessary" + commit-message: + prefix: "fix" + prefix-development: "chore" + include: "scope" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..68f0fb8 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,69 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Build" + +on: + pull_request: + push: + branches: + - "main" + +permissions: + contents: "read" + +jobs: + tests: + name: "Tests" + + runs-on: ${{ matrix.operating-system }} + + strategy: + matrix: + dependencies: ["lowest", "highest"] + php-version: + - "8.4" + - "8.5" + operating-system: ["ubuntu-latest"] + + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Determine composer cache directory" + id: "composer-cache" + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: "Cache dependencies" + uses: "actions/cache@v4" + with: + path: "${{ steps.composer-cache.outputs.dir }}" + key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}-composer-${{ hashFiles('**/composer.json') }}" + restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}-composer-" + + - name: "Validate composer.json" + run: "composer validate --strict" + + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --prefer-dist --no-interaction --no-progress" + + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --prefer-dist --no-interaction --no-progress" + + - name: "Tests" + run: "vendor/bin/phpunit" + + - name: "Coding standards" + if: ${{ matrix.dependencies == 'highest' }} + run: "vendor/bin/phpcs" + + - name: "Static analysis" + if: ${{ matrix.dependencies == 'highest' }} + run: "vendor/bin/phpstan analyse --no-progress" diff --git a/.github/workflows/code_coverage.yaml b/.github/workflows/code_coverage.yaml index f3ce61d..9577a52 100644 --- a/.github/workflows/code_coverage.yaml +++ b/.github/workflows/code_coverage.yaml @@ -1,40 +1,51 @@ -name: Code Coverage +name: "Code Coverage" on: - pull_request: null + pull_request: push: branches: - - main + - "main" + +permissions: + contents: "read" jobs: code_coverage: + name: "Code Coverage" + + runs-on: "ubuntu-latest" - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: shivammathur/setup-php@v2 + - name: "Checkout" + uses: "actions/checkout@v4" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" with: - php-version: 8.4 - coverage: xdebug - - name: Load dependencies from cache - id: composer-cache - run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v4 + coverage: "xdebug" + php-version: "8.5" + + - name: "Determine composer cache directory" + id: "composer-cache" + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: "Cache dependencies" + uses: "actions/cache@v4" with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-php8.4-composer-${{ hashFiles('**/composer.json') }} - restore-keys: | - ${{ runner.os }}-php8.4-composer- + path: "${{ steps.composer-cache.outputs.dir }}" + key: "coverage-php-8.5-composer-${{ hashFiles('**/composer.json') }}" + restore-keys: "coverage-php-8.5-composer-" + + - name: "Install dependencies" + run: "composer update --prefer-dist --no-interaction --no-progress" - - run: composer install --prefer-dist --no-progress --no-suggest - - run: php vendor/bin/phpunit --coverage-clover build/logs/clover.xml + - name: "Tests with coverage" + run: "vendor/bin/phpunit --coverage-clover build/logs/clover.xml" - - uses: codecov/codecov-action@v5 + - name: "Upload coverage" + uses: "codecov/codecov-action@v5" with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - files: build/logs/clover.xml # optional - flags: unittests # optional - name: codecov-umbrella # optional - fail_ci_if_error: true # optional (default = false) - verbose: true # optional (default = false) + token: ${{ secrets.CODECOV_TOKEN }} + files: "build/logs/clover.xml" + flags: "unittests" + fail_ci_if_error: true diff --git a/.github/workflows/coding_standards.yaml b/.github/workflows/coding_standards.yaml deleted file mode 100644 index 4ba1ad9..0000000 --- a/.github/workflows/coding_standards.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Coding Standards - -on: - pull_request: null - push: - branches: - - main - -jobs: - coding_standards: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: shivammathur/setup-php@v2 - with: - php-version: 8.4 - coverage: none - - name: Load dependencies from cache - id: composer-cache - run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-php8.4-composer-${{ hashFiles('**/composer.json') }} - restore-keys: | - ${{ runner.os }}-php8.4-composer- - - - run: composer validate --strict - - run: composer install --prefer-dist --no-progress --no-suggest - - run: php vendor/bin/phpcs diff --git a/.github/workflows/phpstan.yaml b/.github/workflows/phpstan.yaml deleted file mode 100644 index 8104c06..0000000 --- a/.github/workflows/phpstan.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: Static Analysis (PHPStan) - -on: - pull_request: null - push: - branches: - - main - -jobs: - phpstan: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: shivammathur/setup-php@v2 - with: - php-version: 8.4 - coverage: none - - name: Load dependencies from cache - id: composer-cache - run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-php8.4-composer-${{ hashFiles('**/composer.json') }} - restore-keys: | - ${{ runner.os }}-php8.4-composer- - - - run: composer install --prefer-dist --no-progress --no-suggest - - run: composer run-script phpstan diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index fdb881f..0000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Tests - -on: - pull_request: null - push: - branches: - - main - -jobs: - tests: - runs-on: ubuntu-latest - strategy: - matrix: - php-versions: - - 8.3 - - 8.4 - - name: Tests on PHP ${{ matrix.php-versions }} - steps: - - uses: actions/checkout@v4 - - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - coverage: none - - name: Load dependencies from cache - id: composer-cache - run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-php${{ matrix.php-versions }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: | - ${{ runner.os }}-php${{ matrix.php-versions }}-composer- - - - run: composer install --prefer-dist --no-progress --no-suggest - - run: php vendor/bin/phpunit - - tests_lowest_dependencies: - runs-on: ubuntu-latest - name: Tests with lowest dependencies - steps: - - uses: actions/checkout@v4 - - uses: shivammathur/setup-php@v2 - with: - php-version: 8.4 - coverage: none - - name: Load dependencies from cache - id: composer-cache - run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-php8.4-lowest-composer-${{ hashFiles('**/composer.json') }} - restore-keys: | - ${{ runner.os }}-php8.4-lowest-composer- - - run: composer update --no-progress --no-suggest --prefer-lowest - - run: php vendor/bin/phpunit - diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6e9e212 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# CLAUDE.md + +## Projekt + +`brainbits/phpstan-rules` ist eine PHPStan-Extension (Composer-Typ `phpstan-extension`) mit +zwei strikten Regeln für PHPUnit-Testklassen. Reine Library, keine Anwendung. + +- PHP ^8.4. Abhängigkeiten: `phpstan/phpstan` ^2.0, `nette/utils` ^4.0 (php-parser kommt + aus dem PHPStan-PHAR, nicht als eigene Abhängigkeit), + Dev: PHPUnit ^13.4, `phpstan/phpstan-phpunit` ^2.0, `brainbits/phpcs-standard` ^8. +- CI (`.github/workflows/build.yml`): PHP 8.4 und 8.5, jeweils mit `lowest`- und + `highest`-Abhängigkeiten. Bei `highest` laufen zusätzlich phpcs und PHPStan. + `composer validate --strict` muss durchgehen. +- `code_coverage.yaml`: Coverage über Codecov (PHP 8.5, Xdebug). `codecov.yml` erlaubt beim + Projekt-Status 1 % Toleranz. +- `release.yaml`: semantic-release bei Push nach `main` (siehe Release). + +## Aufbau + +- `src/` – Regeln, Namespace `BrainbitsPhpStan\` (PSR-4). + - `CoversClassExistsRule` – meldet `#[CoversClass(...)]`-Attribute und + `@covers`/`@coversDefaultClass`-Annotationen, die auf eine nicht existierende Klasse + zeigen. Nutzt den `ReflectionProvider` (autowired). + - `CoversClassPresentRule` – verlangt in Unit-Tests `@covers`/`@coversDefaultClass` oder + `#[CoversClass]`/`#[CoversFunction]`/`#[CoversNothing]`. Unit-Test heißt: Klasse erbt + von irgendetwas, Namespace enthält `unitTestNamespaceContainsString` (Default `Unit`), + Klassenname endet auf `Test`. +- `rules.neon` – Service-Registrierung (Tag `phpstan.rules.rule`), Parameter + `brainbits.unitTestNamespaceContainsString` samt `parametersSchema`. Wird über + `extra.phpstan.includes` in `composer.json` automatisch eingebunden. Neue Regeln und + Parameter hier registrieren. +- `tests/` – je Regel ein `RuleTestCase` (Classmap-Autoload, Namespace + `BrainbitsPhpStan\Tests`). +- `tests/Fixture//` – PHP-Dateien, die die Tests analysieren. Fixtures für + `CoversClassPresentRule` unter `Unit/` decken die Namespace-Prüfung ab. +- `phpstan.neon.dist` – analysiert `src/` und `tests/` dieses Repos (Level max, + bleedingEdge, eigene `rules.neon`). +- `phpcs.xml.dist` – prüft `src/` und `tests/` mit dem Brainbits-Standard. + +## Befehle + +```bash +composer install + +vendor/bin/phpunit # Tests +vendor/bin/phpunit --filter testAttributeWithUnitRule # einzelner Test +vendor/bin/phpcs # Coding Standard +vendor/bin/phpstan analyse # Statische Analyse +``` + +Es gibt keine Composer-Scripts. + +## Arbeitsweise + +- Beide Regeln arbeiten auf `PhpParser\Node\Stmt\Class_` und prüfen Attribute und + Docblock-Annotationen getrennt. Die Annotationen werden zeilenweise per Regex gelesen; + dieselben Regexe stehen in beiden Regeln – synchron halten. +- Attributnamen werden als voll qualifizierte Strings verglichen + (`PHPUnit\Framework\Attributes\CoversClass`), PHPStan löst die Namen vorher auf. +- Tests prüfen exakte Fehlermeldungen **und Zeilennummern** in den Fixtures. Wer ein + Fixture ändert, verschiebt Zeilen – erwartete Zeilennummern im Test nachziehen. +- Fixtures sind absichtlich von phpcs und PHPStan ausgenommen (sie enthalten gewollt + fehlerhaften Code). Nicht "reparieren". +- Code muss den Brainbits-Standard erfüllen: `declare(strict_types=1);`, importierte + Funktionen (`use function`), finale Klassen, native Typen. +- CI testet auch `--prefer-lowest`, Versionsconstraints also nicht höher setzen als nötig, + aber auch nicht so niedrig, dass die niedrigste Version die Tests bricht. +- Regelverhalten oder Parameter geändert → `README.md` anpassen. +- Abhängigkeiten aktualisiert Dependabot (`.github/dependabot.yml`: GitHub Actions und + Composer). + +## Release + +semantic-release bei Push nach `main` (`.releaserc`, Tags ohne `v`-Präfix). Commits im Stil +Conventional Commits (`feat:`, `fix:`, `fix(deps):`, `docs:`, `chore:`, `ci:`, `test:`) – +sie bestimmen die Versionsnummer. Entwicklungsbranch ist `main`; `master` ist veraltet. diff --git a/README.md b/README.md index fb53240..b31c188 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # PHPStan Clean Test rules -![Continuous Integration](https://github.com/brainbits/phpstan-rules/workflows/Tests/badge.svg) -[![Coverage Status](https://coveralls.io/repos/github/brainbits/phpstan-rules/badge.svg?branch=master)](https://coveralls.io/github/brainbits/phpstan-rules?branch=master) +[![Build](https://github.com/brainbits/phpstan-rules/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/brainbits/phpstan-rules/actions/workflows/build.yml) +[![codecov](https://codecov.io/gh/brainbits/phpstan-rules/branch/main/graph/badge.svg)](https://codecov.io/gh/brainbits/phpstan-rules) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/881bcaeeaa2a45f59b0a77680f15fafe)](https://www.codacy.com/manual/brainbits/phpstan-rules?utm_source=github.com&utm_medium=referral&utm_content=brainbits/phpstan-rules&utm_campaign=Badge_Grade) [![Latest Stable Version](https://poser.pugx.org/brainbits/phpstan-rules/version)](https://packagist.org/packages/brainbits/phpstan-rules) [![License](https://poser.pugx.org/brainbits/phpstan-rules/license)](https://packagist.org/packages/brainbits/phpstan-rules) @@ -21,7 +21,7 @@ $ composer require --dev brainbits/phpstan-rules ## Usage -All of the [rules](https://github.com/brainbits/phpstan-rules#rules) provided by this library are included in [`rules.neon`](rules.neon). +All of the [rules](#rules) provided by this library are included in [`rules.neon`](rules.neon). When you are using [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer), `rules.neon` will be automatically included. @@ -36,8 +36,8 @@ includes: ## Rules This package provides the following rules for use with [`phpstan/phpstan`](https://github.com/phpstan/phpstan): -- [`Brainbits\PHPStan\Rules\CoversClassExistsRule`](#CoversClassExistsRule) -- [`Brainbits\PHPStan\Rules\CoversClassPresentRule`](#CoversClassPresentRule) +- [`BrainbitsPhpStan\CoversClassExistsRule`](#CoversClassExistsRule) +- [`BrainbitsPhpStan\CoversClassPresentRule`](#CoversClassPresentRule) ### `CoversClassExistsRule` @@ -45,7 +45,7 @@ This rule checks that classes that are covered by `@covers` annotation or `#[Cov ### `CoversClassPresentRule` -This rule forces you to specify either a `@covers` annotation or `#[CoversClass]`, `#[CoversFunction]` or `#[CoversNothing]` attributes in unit tests (default: `PHPUnit\Framework\TestCase`). +This rule forces you to specify either a `@covers` annotation or `#[CoversClass]`, `#[CoversFunction]` or `#[CoversNothing]` attributes in unit tests. **Why:** 1. It prevents code coverage sums to show higher values than expected. @@ -66,13 +66,13 @@ final class MyInvalidClassTest extends \PHPUnit\Framework\TestCase {} // tests/ExampleTestCase/Unit/MyClassTest.php namespace ExampleTestCase\Unit; -#[\PHPUnit\Framework\Attributes\CoversClass(MyClass::class) +#[\PHPUnit\Framework\Attributes\CoversClass(MyClass::class)] final class MyClassTest extends \PHPUnit\Framework\TestCase {} ``` #### Defaults -- By default, this rule detects unit tests by checking the namespace (it must contain the string `Unit`) and the class name ending (it must end with the string `Test`). +- By default, this rule treats a class as a unit test if it extends another class, its namespace contains the string `Unit` and its name ends with `Test`. The parent class itself is not checked. #### Detecting unit tests namespace If you want to change the namespace string check described above, you can set your own string to be checked in the `unitTestNamespaceContainsString` parameter. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..236a6f1 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,5 @@ +coverage: + status: + project: + default: + threshold: 1% diff --git a/composer.json b/composer.json index dc1d69c..5b6292e 100644 --- a/composer.json +++ b/composer.json @@ -17,24 +17,14 @@ ], "minimum-stability": "stable", "require": { - "php": "^8.3", + "php": "^8.4", "nette/utils": "^4.0", - "nikic/php-parser": "^4.3|^5.0", "phpstan/phpstan": "^2.0" }, "require-dev": { "brainbits/phpcs-standard": "^8.0", - "php-coveralls/php-coveralls": "^2.0", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^12.0" - }, - "scripts": { - "check-all": [ - "phpcs", - "@phpstan", - "phpunit" - ], - "phpstan": "phpstan analyse --ansi" + "phpunit/phpunit": "^13.4" }, "autoload": { "psr-4": { @@ -54,9 +44,6 @@ } }, "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "rules.neon" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3f786b9..6973773 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + tests diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 5db72dd..0000000 --- a/renovate.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ] -} diff --git a/src/CoversClassExistsRule.php b/src/CoversClassExistsRule.php index 5f98851..c70ca5b 100644 --- a/src/CoversClassExistsRule.php +++ b/src/CoversClassExistsRule.php @@ -59,7 +59,7 @@ public function processNodeAttribute(Class_ $node, Scope $scope): array } foreach ($node->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $name => $attr) { + foreach ($attrGroup->attrs as $attr) { if ((string) $attr->name !== 'PHPUnit\Framework\Attributes\CoversClass') { continue; } diff --git a/src/CoversClassPresentRule.php b/src/CoversClassPresentRule.php index f7dba0a..64d1df0 100644 --- a/src/CoversClassPresentRule.php +++ b/src/CoversClassPresentRule.php @@ -90,7 +90,7 @@ public function processNodeAttribute(Class_ $node, Scope $scope): bool } foreach ($node->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $name => $attr) { + foreach ($attrGroup->attrs as $attr) { if ((string) $attr->name === 'PHPUnit\Framework\Attributes\CoversClass') { return true; }