From 4cd913a5d5c4bdbf3dfe31a61ba03d45fc6c53ee Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sat, 20 Jul 2024 07:24:59 +0200 Subject: [PATCH] Replace Makefile with castor.php and update workflow A new castor.php file is introduced, replacing the existing Makefile which has been removed. This update modifies the GitHub workflows to call the appropriate Castor tasks. Minor changes are also made to existing source files and static analysis configurations to include the new castor.php file. --- .gitattributes | 3 +- .github/workflows/infection.yml | 3 +- .github/workflows/integrate.yml | 24 +- Makefile | 57 ----- castor.php | 215 ++++++++++++++++++ ecs.php | 4 +- phpstan-baseline.neon | 10 - rector.php | 10 +- sonar-project.properties | 4 - .../Guesser/RequestBodyUserEntityGuesser.php | 5 +- .../CheckClientDataCollectorType.php | 4 +- .../src/CeremonyStep/CheckSignature.php | 4 +- 12 files changed, 244 insertions(+), 99 deletions(-) delete mode 100644 Makefile create mode 100644 castor.php delete mode 100644 sonar-project.properties diff --git a/.gitattributes b/.gitattributes index 5c7ac9d0..63a4775a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,15 +11,14 @@ /.mergify.yml export-ignore /babel.config.js export-ignore /CODE_OF_CONDUCT.md export-ignore +/castor.php export-ignore /deptrac.yaml export-ignore /ecs.php export-ignore /infection.json export-ignore /jest.config.js export-ignore -/Makefile export-ignore /phpstan.neon export-ignore /phpstan-baseline.neon export-ignore /phpunit.xml.dist export-ignore /rector.php export-ignore /rollup.config.js export-ignore -/sonar-project.properties export-ignore /tsconfig.json export-ignore diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index 2f87de72..97bdab52 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -15,6 +15,7 @@ jobs: with: php-version: "8.3" extensions: "ctype, curl, dom, json, libxml, mbstring, openssl, phar, simplexml, sodium, tokenizer, xml, xmlwriter, zlib" + tools: "castor" coverage: "xdebug" - name: "Checkout code" @@ -30,4 +31,4 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute Infection" - run: "make ci-mu" + run: "castor infect" diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index f7a76d10..eac003ae 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -32,6 +32,7 @@ jobs: with: php-version: "8.3" coverage: "none" + tools: "castor" - name: "Checkout code" uses: "actions/checkout@v4" @@ -42,7 +43,7 @@ jobs: dependency-versions: "highest" - name: "Check source code for syntax errors" - run: "composer exec -- parallel-lint src/ tests/" + run: "castor lint" php_tests: name: "2️⃣ Unit and functional tests" @@ -66,6 +67,7 @@ jobs: with: php-version: "${{ matrix.php-version }}" extensions: "ctype, curl, dom, json, libxml, mbstring, openssl, phar, simplexml, sodium, tokenizer, xml, xmlwriter, zlib" + tools: "castor" coverage: "xdebug" - name: "Checkout code" @@ -80,10 +82,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute tests (PHP)" - run: "make ci-cc" - - - name: "Fix code coverage paths" - run: sed -i 's@'$GITHUB_WORKSPACE'@/github/workspace/@g' coverage.xml + run: "castor test" js_tests: name: "2️⃣ JS tests" @@ -103,6 +102,7 @@ jobs: with: php-version: "${{ matrix.php-version }}" extensions: "ctype, curl, dom, json, libxml, mbstring, openssl, phar, simplexml, sodium, tokenizer, xml, xmlwriter, zlib" + tools: "castor" coverage: "xdebug" - name: "Checkout code" @@ -117,7 +117,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute tests (JS)" - run: "make js" + run: "castor js" static_analysis: name: "3️⃣ Static Analysis" @@ -131,6 +131,7 @@ jobs: with: php-version: "8.3" extensions: "ctype, curl, dom, json, libxml, mbstring, openssl, phar, simplexml, sodium, tokenizer, xml, xmlwriter, zlib" + tools: "castor" coverage: "none" - name: "Checkout code" @@ -149,7 +150,7 @@ jobs: run: "composer dump-autoload --optimize --strict-psr" - name: "Execute static analysis" - run: "make st" + run: "castor stan" coding_standards: name: "4️⃣ Coding Standards" @@ -163,6 +164,7 @@ jobs: with: php-version: "8.3" extensions: "ctype, curl, dom, json, libxml, mbstring, openssl, phar, simplexml, sodium, tokenizer, xml, xmlwriter, zlib" + tools: "castor" coverage: "none" - name: "Checkout code" @@ -181,11 +183,10 @@ jobs: composer-options: "--optimize-autoloader" - name: "Check coding style" - run: "make ci-cs" + run: "castor cs" - name: "Deptrac" - run: | - vendor/bin/deptrac analyse --fail-on-uncovered --no-cache + run: "castor deptrac" rector_checkstyle: name: "6️⃣ Rector Checkstyle" @@ -199,6 +200,7 @@ jobs: with: php-version: "8.3" extensions: "ctype, curl, dom, json, libxml, mbstring, openssl, phar, simplexml, sodium, tokenizer, xml, xmlwriter, zlib" + tools: "castor" coverage: "xdebug" - name: "Checkout code" @@ -214,7 +216,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute Rector" - run: "make rector" + run: "castor rector" exported_files: name: "7️⃣ Exported files" diff --git a/Makefile b/Makefile deleted file mode 100644 index e5f4cabd..00000000 --- a/Makefile +++ /dev/null @@ -1,57 +0,0 @@ -.PHONY: mu -mu: vendor ## Mutation tests - vendor/bin/infection -v -s --threads=$$(nproc) --min-msi=45 --min-covered-msi=60 - -.PHONY: tests -tests: vendor ## Run all tests - bin/phpunit --color - yarn test - -.PHONY: cc -cc: vendor ## Show test coverage rates (HTML) - XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html ./build - -.PHONY: cs -cs: vendor ## Fix all files using defined ECS rules - vendor/bin/ecs check --fix - -.PHONY: st -st: vendor ## Run static analyse - XDEBUG_MODE=off vendor/bin/phpstan analyse - - -################################################ - -.PHONY: ci-mu -ci-mu: vendor ## Mutation tests (for CI/CD only) - vendor/bin/infection --logger-github -s --threads=$$(nproc) --min-msi=45 --min-covered-msi=60 - -.PHONY: ci-cc -ci-cc: vendor ## Show test coverage rates (for CI/CD only) - XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml - -.PHONY: ci-cs -ci-cs: vendor ## Check all files using defined ECS rules (for CI/CD only) - XDEBUG_MODE=off vendor/bin/ecs check - -################################################ - - -js: node_modules ## Execute JS tests - yarn test - -node_modules: package.json - yarn install --force - -.PHONY: rector -rector: vendor ## Check all files using Rector - XDEBUG_MODE=off vendor/bin/rector process --ansi --dry-run --xdebug - -vendor: composer.json - composer validate - composer install - -.DEFAULT_GOAL := help -help: - @grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' -.PHONY: help diff --git a/castor.php b/castor.php new file mode 100644 index 00000000..92abc478 --- /dev/null +++ b/castor.php @@ -0,0 +1,215 @@ +title('Running infection'); + $nproc = run('nproc', quiet: true); + if (! $nproc->isSuccessful()) { + io()->error('Cannot determine the number of processors'); + return; + } + $threads = (int) $nproc->getOutput(); + $command = [ + 'php', + 'vendor/bin/infection', + sprintf('--min-msi=%s', $minMsi), + sprintf('--min-covered-msi=%s', $minCoveredMsi), + sprintf('--threads=%s', $threads), + ]; + if ($ci) { + $command[] = '--logger-github'; + $command[] = '-s'; + } + $environment = [ + 'XDEBUG_MODE' => 'coverage', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Run tests')] +function test(bool $coverageHtml = false, bool $coverageText = false, null|string $group = null): void +{ + io()->title('Running tests'); + $command = ['php', 'vendor/bin/phpunit', '--color']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + if ($coverageHtml) { + $command[] = '--coverage-html=build/coverage'; + $environment['XDEBUG_MODE'] = 'coverage'; + } + if ($coverageText) { + $command[] = '--coverage-text'; + $environment['XDEBUG_MODE'] = 'coverage'; + } + if ($group !== null) { + $command[] = sprintf('--group=%s', $group); + } + run($command, environment: $environment); +} + +#[AsTask(description: 'Coding standards check')] +function cs( + #[AsOption(description: 'Fix issues if possible')] + bool $fix = false, + #[AsOption(description: 'Clear cache')] + bool $clearCache = false +): void { + io()->title('Running coding standards check'); + $command = ['php', 'vendor/bin/ecs', 'check']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + if ($fix) { + $command[] = '--fix'; + } + if ($clearCache) { + $command[] = '--clear-cache'; + } + run($command, environment: $environment); +} + +#[AsTask(description: 'Running PHPStan')] +function stan(bool $baseline = false): void +{ + io()->title('Running PHPStan'); + $command = ['php', 'vendor/bin/phpstan', 'analyse']; + if ($baseline) { + $command[] = '--generate-baseline'; + } + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Validate Composer configuration')] +function validate(): void +{ + io()->title('Validating Composer configuration'); + $command = ['composer', 'validate', '--strict']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); + + $command = ['composer', 'dump-autoload', '--optimize', '--strict-psr']; + run($command, environment: $environment); +} + +/** + * @param array $allowedLicenses + */ +#[AsTask(description: 'Check licenses')] +function checkLicenses( + array $allowedLicenses = ['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MIT', 'MPL-2.0', 'OSL-3.0'] +): void { + io()->title('Checking licenses'); + $allowedExceptions = []; + $command = ['composer', 'licenses', '-f', 'json']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + $result = run($command, environment: $environment, quiet: true); + if (! $result->isSuccessful()) { + io()->error('Cannot determine licenses'); + exit(1); + } + $licenses = json_decode((string) $result->getOutput(), true); + $disallowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => ! in_array($name, $allowedExceptions, true) + && count(array_diff($info['license'], $allowedLicenses)) === 1, + \ARRAY_FILTER_USE_BOTH + ); + $allowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => in_array($name, $allowedExceptions, true) + || count(array_diff($info['license'], $allowedLicenses)) === 0, + \ARRAY_FILTER_USE_BOTH + ); + if (count($disallowed) > 0) { + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($disallowed), + $disallowed + ) + ); + io() + ->error('Disallowed licenses found'); + exit(1); + } + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($allowed), + $allowed + ) + ); + io() + ->success('All licenses are allowed'); +} + +#[AsTask(description: 'Run Rector')] +function rector( + #[AsOption(description: 'Fix issues if possible')] + bool $fix = false, + #[AsOption(description: 'Clear cache')] + bool $clearCache = false +): void { + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/rector', 'process', '--ansi']; + if (! $fix) { + $command[] = '--dry-run'; + } + if ($clearCache) { + $command[] = '--clear-cache'; + } + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Run Rector')] +function deptrac(): void +{ + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/deptrac', 'analyse', '--fail-on-uncovered', '--no-cache']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Run Linter')] +function lint(): void +{ + io()->title('Running Linter'); + $command = ['composer', 'exec', '--', 'parallel-lint', __DIR__ . '/src/', __DIR__ . '/tests/']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Run JS tests')] +function js(): void +{ + io()->title('Running JS tests'); + run(['npm', 'install', '--force']); + run(['npm', 'test']); +} diff --git a/ecs.php b/ecs.php index 01a26af3..8eeae904 100644 --- a/ecs.php +++ b/ecs.php @@ -89,5 +89,7 @@ ]); $config->parallel(); - $config->paths([__DIR__ . '/src', __DIR__ . '/tests', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']); + $config->paths( + [__DIR__ . '/src', __DIR__ . '/tests', __DIR__ . '/ecs.php', __DIR__ . '/rector.php', __DIR__ . '/castor.php'] + ); }; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 26c51fdf..bc60ad2b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -365,16 +365,6 @@ parameters: count: 1 path: src/symfony/src/Security/Authentication/Token/WebauthnToken.php - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpFoundation\\\\Request\\:\\:getContentType\\(\\)\\.$#" - count: 1 - path: src/symfony/src/Security/Guesser/RequestBodyUserEntityGuesser.php - - - - message: "#^Call to function method_exists\\(\\) with Symfony\\\\Component\\\\HttpFoundation\\\\Request and 'getContentTypeFormat' will always evaluate to true\\.$#" - count: 1 - path: src/symfony/src/Security/Guesser/RequestBodyUserEntityGuesser.php - - message: "#^Method Webauthn\\\\Bundle\\\\Security\\\\Http\\\\Authenticator\\\\WebauthnAuthenticator\\:\\:__construct\\(\\) has parameter \\$userProvider with generic interface Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserProviderInterface but does not specify its types\\: TUser$#" count: 1 diff --git a/rector.php b/rector.php index 37b5df89..0553b10e 100644 --- a/rector.php +++ b/rector.php @@ -9,7 +9,6 @@ use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector; //use Rector\PHPUnit\Set\PHPUnitLevelSetList; use Rector\PHPUnit\Set\PHPUnitSetList; -use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; //use Rector\Symfony\Set\SymfonyLevelSetList; use Rector\Symfony\Set\SymfonySetList; @@ -17,8 +16,7 @@ return static function (RectorConfig $config): void { $config->import(SetList::DEAD_CODE); - $config->import(LevelSetList::UP_TO_PHP_82); - $config->import(SymfonySetList::SYMFONY_60); + $config->import(SymfonySetList::SYMFONY_64); $config->import(SymfonySetList::SYMFONY_50_TYPES); $config->import(SymfonySetList::SYMFONY_52_VALIDATOR_ATTRIBUTES); $config->import(SymfonySetList::SYMFONY_CODE_QUALITY); @@ -28,8 +26,10 @@ $config->import(DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES); $config->import(PHPUnitSetList::PHPUNIT_CODE_QUALITY); $config->import(PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES); - $config->import(PHPUnitSetList::PHPUNIT_100); - $config->paths([__DIR__ . '/src', __DIR__ . '/tests', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']); + $config->import(PHPUnitSetList::PHPUNIT_110); + $config->paths( + [__DIR__ . '/src', __DIR__ . '/tests', __DIR__ . '/ecs.php', __DIR__ . '/rector.php', __DIR__ . '/castor.php'] + ); $config->skip([ __DIR__ . '/src/symfony/src/DependencyInjection/Configuration.php', __DIR__ . '/src/symfony/src/Routing/Loader.php', diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 9a22eb7b..00000000 --- a/sonar-project.properties +++ /dev/null @@ -1,4 +0,0 @@ -sonar.organization=web-auth -sonar.projectKey=web-auth_webauthn-framework -sonar.php.coverage.reportPaths=coverage.xml -sonar.sources=src diff --git a/src/symfony/src/Security/Guesser/RequestBodyUserEntityGuesser.php b/src/symfony/src/Security/Guesser/RequestBodyUserEntityGuesser.php index f09f39bd..0773317d 100644 --- a/src/symfony/src/Security/Guesser/RequestBodyUserEntityGuesser.php +++ b/src/symfony/src/Security/Guesser/RequestBodyUserEntityGuesser.php @@ -27,10 +27,7 @@ public function __construct( public function findUserEntity(Request $request): PublicKeyCredentialUserEntity { - $format = method_exists( - $request, - 'getContentTypeFormat' - ) ? $request->getContentTypeFormat() : $request->getContentType(); + $format = $request->getContentTypeFormat(); $format === 'json' || throw InvalidDataException::create($format, 'Only JSON content type allowed'); $content = $request->getContent(); diff --git a/src/webauthn/src/CeremonyStep/CheckClientDataCollectorType.php b/src/webauthn/src/CeremonyStep/CheckClientDataCollectorType.php index 645cb904..1dd256c4 100644 --- a/src/webauthn/src/CeremonyStep/CheckClientDataCollectorType.php +++ b/src/webauthn/src/CeremonyStep/CheckClientDataCollectorType.php @@ -12,9 +12,9 @@ use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialSource; -final class CheckClientDataCollectorType implements CeremonyStep +final readonly class CheckClientDataCollectorType implements CeremonyStep { - private readonly ClientDataCollectorManager $clientDataCollectorManager; + private ClientDataCollectorManager $clientDataCollectorManager; public function __construct( null|ClientDataCollectorManager $clientDataCollectorManager = null, diff --git a/src/webauthn/src/CeremonyStep/CheckSignature.php b/src/webauthn/src/CeremonyStep/CheckSignature.php index a89fef32..6ce7721c 100644 --- a/src/webauthn/src/CeremonyStep/CheckSignature.php +++ b/src/webauthn/src/CeremonyStep/CheckSignature.php @@ -22,9 +22,9 @@ use Webauthn\Util\CoseSignatureFixer; use function is_array; -final class CheckSignature implements CeremonyStep +final readonly class CheckSignature implements CeremonyStep { - private readonly Manager $algorithmManager; + private Manager $algorithmManager; public function __construct( null|Manager $algorithmManager = null,