diff --git a/.gitattributes b/.gitattributes index 5c7ac9d05..63a4775a8 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 2f87de72f..97bdab528 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 f7a76d10d..eac003ae2 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 e5f4cabd0..000000000 --- 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 000000000..92abc478c --- /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 01a26af34..8eeae9041 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 26c51fdf5..bc60ad2b0 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 37b5df895..0553b10e6 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 9a22eb7bb..000000000 --- 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 f09f39bde..0773317d8 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 645cb904d..1dd256c49 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 a89fef324..6ce7721cd 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,