diff --git a/.composer-require-checker.json b/.composer-require-checker.json new file mode 100644 index 0000000..8f13040 --- /dev/null +++ b/.composer-require-checker.json @@ -0,0 +1,34 @@ +{ + "symbol-whitelist": [ + "null", + "true", + "false", + "static", + "self", + "parent", + "array", + "string", + "int", + "float", + "bool", + "callable", + "iterable", + "void", + "object", + "mixed", + "never", + "PHPStan\\Analyser\\Scope", + "PHPStan\\Node\\InClassMethodNode", + "PHPStan\\Reflection\\Php\\PhpFunctionFromParserNodeReflection", + "PHPStan\\Rules\\Rule", + "PHPStan\\Rules\\RuleErrorBuilder" + ], + "php-core-extensions": [ + "Core", + "standard", + "json", + "pcre", + "SPL" + ], + "scan-files": [] +} diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000..8eeb07f --- /dev/null +++ b/.github/workflows/actionlint.yml @@ -0,0 +1,21 @@ +name: actionlint + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + actionlint: + name: Lint GitHub Actions workflows + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Run actionlint + uses: rhysd/actionlint@v1.7.10 diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml new file mode 100644 index 0000000..cc9646a --- /dev/null +++ b/.github/workflows/composer-require-checker.yml @@ -0,0 +1,40 @@ +name: Composer Require Checker + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + composer-require-checker: + name: Run Composer Require Checker + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: json, curl + coverage: none + tools: composer:v2, composer-require-checker + + - name: Cache Composer dependencies + uses: actions/cache@v5 + with: + path: vendor + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Run Composer Require Checker + run: composer-require-checker check --config-file=.composer-require-checker.json diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml new file mode 100644 index 0000000..3aa92c8 --- /dev/null +++ b/.github/workflows/rector.yml @@ -0,0 +1,40 @@ +name: Rector + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + rector: + name: Run Rector (dry-run) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: json, curl + coverage: none + tools: composer:v2 + + - name: Cache Composer dependencies + uses: actions/cache@v5 + with: + path: vendor + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Run Rector (dry-run) + run: composer rector diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b2d7890 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,71 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + name: PHP ${{ matrix.php-version }} Tests + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php-version: + - '8.2' + - '8.3' + - '8.4' + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: curl, json, mbstring + coverage: xdebug + tools: composer:v2 + + - name: Get composer cache directory + id: composer-cache + run: | + { + printf dir= + composer config cache-files-dir + } > "$GITHUB_OUTPUT" + + - name: Cache dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Run tests + if: matrix.php-version != '8.2' + run: composer test + + - name: Run tests with coverage + if: matrix.php-version == '8.2' + run: | + echo "::group::Running PHPUnit with Coverage" + vendor/bin/phpunit --coverage-text --coverage-html coverage-report --colors=never + echo "::endgroup::" + + - name: Upload coverage report + if: matrix.php-version == '8.2' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage-report/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 7b37592..e20c424 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /vendor/ composer.lock .phpunit.result.cache +.phpunit.cache/ +coverage-report/ .php-cs-fixer.cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 85c0683..5538f5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,8 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml + args: + - --unsafe - id: check-added-large-files - id: check-merge-conflict - id: check-json @@ -29,6 +31,11 @@ repos: - --autofix - --indent=2 +- repo: https://github.com/rhysd/actionlint + rev: v1.7.10 + hooks: + - id: actionlint + - repo: local hooks: - id: php-syntax-check @@ -75,3 +82,11 @@ repos: types: - php pass_filenames: false + + - id: rector + name: Rector Code Modernization + entry: composer run-script rector-fix + language: system + types: + - php + pass_filenames: false diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..b155aff --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,86 @@ +# https://taskfile.dev +# Task runner for code quality checks +# +# SEPARATION OF CONCERNS: +# Taskfile: Convenience wrappers for common operations +# Composer: Core code quality checks (phpcs, phpstan, rector) +# +# Installation: https://taskfile.dev/installation/ +# macOS: brew install go-task +# Linux: sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin +# +# Usage: +# task --list # Show all available tasks +# task check # Run all code quality checks +# task fix # Auto-fix code style issues + +version: '3' + +tasks: + default: + desc: Show available tasks + cmds: + - task --list + + # === Code Quality (delegates to Composer scripts) === + + check: + desc: Run all code quality checks (calls pre-commit run -a) + cmds: + - pre-commit run -a + + check:phpcs: + desc: Run PHP CodeSniffer (calls composer phpcs) + cmds: + - composer phpcs + + check:phpstan: + desc: Run PHPStan (calls composer phpstan) + cmds: + - composer phpstan + + check:rector: + desc: Run Rector dry-run (calls composer rector) + cmds: + - composer rector + + fix: + desc: Auto-fix code style (calls composer fix) + cmds: + - composer fix + + fix:phpcs: + desc: Auto-fix PHP CodeSniffer issues (calls composer phpcbf) + cmds: + - composer phpcbf + + fix:rector: + desc: Apply Rector fixes (calls composer rector-fix) + cmds: + - composer rector-fix + + # === Testing === + + test: + desc: Run PHPUnit tests + cmds: + - composer test + + # === Development Helpers === + + composer:install: + desc: Install composer dependencies + cmds: + - composer install + - echo "Composer dependencies installed" + + composer:update: + desc: Update composer dependencies + cmds: + - composer update + - echo "Composer dependencies updated" + + lint: + desc: Run PHP syntax check + cmds: + - composer php-lint diff --git a/composer.json b/composer.json index f316b50..91af838 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,13 @@ ], "require": { "php": "^8.2", + "nikic/php-parser": "^5.0", "phpstan/phpstan": "^2.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.42", + "phpunit/phpunit": "^11.0", + "rector/rector": "^2.0", "squizlabs/php_codesniffer": "^4.0" }, "autoload": { @@ -28,6 +31,11 @@ "OpenCoreEMR\\PHPStan\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "OpenCoreEMR\\PHPStan\\Tests\\": "tests/" + } + }, "config": { "allow-plugins": { "ergebnis/composer-normalize": true @@ -43,11 +51,19 @@ "scripts": { "check": [ "@phpcs", - "@phpstan" + "@phpstan", + "@rector" + ], + "fix": [ + "@phpcbf", + "@rector-fix" ], "php-lint": "git ls-files -z '*.php' | xargs -0 -n1 php -l", "phpcbf": "phpcbf", "phpcs": "phpcs", - "phpstan": "phpstan analyse -c phpstan.neon src" + "phpstan": "phpstan analyse -c phpstan.neon src", + "rector": "rector process --dry-run", + "rector-fix": "rector process", + "test": "phpunit" } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..7a76898 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,24 @@ + + + + + tests/Unit + + + + + + src + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..f0c3115 --- /dev/null +++ b/rector.php @@ -0,0 +1,37 @@ +withPaths([ + __DIR__ . '/src', + ]) + ->withCache( + cacheClass: FileCacheStorage::class, + cacheDirectory: '/tmp/rector' + ) + ->withConfiguredRule(ClassPropertyAssignToConstructorPromotionRector::class, [ + 'allow_model_based_classes' => true, + 'inline_public' => false, + 'rename_property' => true, + ]) + ->withParallel( + timeoutSeconds: 120, + maxNumberOfProcess: 12, + jobSize: 12 + ) + ->withPhpSets() + ->withPhpVersion(PhpVersion::PHP_82) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + typeDeclarations: true + ) + ->withSkip([ + __DIR__ . '/vendor', + ]); diff --git a/src/Rules/Database/ForbiddenClassesRule.php b/src/Rules/Database/ForbiddenClassesRule.php index d01e16d..76cdc40 100644 --- a/src/Rules/Database/ForbiddenClassesRule.php +++ b/src/Rules/Database/ForbiddenClassesRule.php @@ -46,13 +46,13 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - return iterator_to_array($this->getErrors($node, $scope)); + return iterator_to_array($this->getErrors($node)); } /** * @return \Generator<\PHPStan\Rules\RuleError> */ - private function getErrors(Use_ $node, Scope $scope): \Generator + private function getErrors(Use_ $node): \Generator { foreach ($node->uses as $use) { $importedName = $use->name->toString(); diff --git a/src/Rules/Module/ControllersMustReturnResponseRule.php b/src/Rules/Module/ControllersMustReturnResponseRule.php index e0bba64..8a13969 100644 --- a/src/Rules/Module/ControllersMustReturnResponseRule.php +++ b/src/Rules/Module/ControllersMustReturnResponseRule.php @@ -74,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array // Check if return type is void $function = $scope->getFunction(); - if ($function !== null) { + if ($function instanceof \PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection) { $variants = $function->getVariants(); if (count($variants) > 0) { $returnType = $variants[0]->getReturnType(); diff --git a/src/Rules/Module/NoLegacyResponseMethodsRule.php b/src/Rules/Module/NoLegacyResponseMethodsRule.php index 7cc90ab..f4cf81e 100644 --- a/src/Rules/Module/NoLegacyResponseMethodsRule.php +++ b/src/Rules/Module/NoLegacyResponseMethodsRule.php @@ -41,7 +41,6 @@ public function getNodeType(): string } /** - * @param Node $node * @return array<\PHPStan\Rules\RuleError> */ public function processNode(Node $node, Scope $scope): array diff --git a/src/Rules/Testing/NoCoversAnnotationOnClassRule.php b/src/Rules/Testing/NoCoversAnnotationOnClassRule.php index e990fe0..5e7e99e 100644 --- a/src/Rules/Testing/NoCoversAnnotationOnClassRule.php +++ b/src/Rules/Testing/NoCoversAnnotationOnClassRule.php @@ -38,7 +38,7 @@ public function processNode(Node $node, Scope $scope): array { $docComment = $node->getDocComment(); - if ($docComment === null) { + if (!$docComment instanceof \PhpParser\Comment\Doc) { return []; } diff --git a/tests/Unit/.gitkeep b/tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..a075e1e --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ +