From a8b56eaf32ebc6cd8900537690f00bbda69cff58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Tue, 19 May 2026 15:05:55 +0100 Subject: [PATCH 1/6] Add psalm --- .github/workflows/continuous-integration.yml | 3 +++ .gitignore | 1 + bin/code-tools | 5 ++++ composer.json | 7 +++++- dist/psalm.xml.dist | 20 +++++++++++++++ psalm.xml | 26 ++++++++++++++++++++ 6 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 dist/psalm.xml.dist create mode 100644 psalm.xml diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index de9023b..031d4d6 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -55,6 +55,9 @@ jobs: - name: Run Rector run: vendor/bin/rector process --ansi --dry-run --config rector.php Kununu/ tests/ + - name: Run Psalm + run: vendor/bin/psalm --no-cache + build: needs: checks name: PHPUnit diff --git a/.gitignore b/.gitignore index 1f1aa01..4c120ba 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ composer.phar .DS_Store *.local +CLAUDE.md .claude .cursor diff --git a/bin/code-tools b/bin/code-tools index 178cf37..6bb4b03 100755 --- a/bin/code-tools +++ b/bin/code-tools @@ -20,6 +20,7 @@ fi readonly CONFIG_FILES=( "dist/php-cs-fixer.php.dist" "dist/phpcs.xml.dist" + "dist/psalm.xml.dist" "dist/rector.php.dist" "dist/.editorconfig.dist" ) @@ -32,6 +33,7 @@ Usage: vendor/bin/code-tools publish:config [tool-name] Valid tool names: cs-fixer => copies dist/php-cs-fixer.php.dist code-sniffer => copies dist/phpcs.xml.dist + psalm => copies dist/psalm.xml.dist rector => copies dist/rector.php.dist editorconfig => copies dist/.editorconfig.dist EOF @@ -79,6 +81,9 @@ case "$tool_name" in code-sniffer) copy_config_file "dist/phpcs.xml.dist" ;; + psalm) + copy_config_file "dist/psalm.xml.dist" + ;; rector) copy_config_file "dist/rector.php.dist" ;; diff --git a/composer.json b/composer.json index 1874d6d..420f5fc 100644 --- a/composer.json +++ b/composer.json @@ -16,11 +16,13 @@ "friendsofphp/php-cs-fixer": "^3.93", "phpat/phpat": "^0.11.4", "phpstan/phpstan": "^2.1", + "psalm/plugin-symfony": "^5.3", "rector/rector": "^2.0", "squizlabs/php_codesniffer": "^3.10", "symfony/console": "^6.4 || ^7.4", "symfony/process": "^6.4 || ^7.4", - "symfony/yaml": "^6.4 || ^7.4" + "symfony/yaml": "^6.4 || ^7.4", + "vimeo/psalm": "^6.16" }, "require-dev": { "ergebnis/composer-normalize": "^2.50", @@ -57,12 +59,15 @@ "@cs-check", "@stan", "@rector", + "@psalm", "@test-unit" ], "cs-check": "phpcs --standard=phpcs.xml Kununu/ tests/", "cs-fix": "phpcbf --standard=phpcs.xml Kununu/ tests/", "cs-fixer-check": "php-cs-fixer check --config=php-cs-fixer.php", "cs-fixer-fix": "php-cs-fixer fix --config=php-cs-fixer.php", + "psalm": "psalm --no-cache", + "psalm-baseline": "psalm --no-cache --set-baseline=psalm-baseline.xml", "rector": "rector process --dry-run Kununu/ tests/", "rector-fix": "rector process Kununu/ tests/", "stan": "phpstan analyze", diff --git a/dist/psalm.xml.dist b/dist/psalm.xml.dist new file mode 100644 index 0000000..da88aa4 --- /dev/null +++ b/dist/psalm.xml.dist @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..69d4147 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + From 5edfaa3dbfea70c25db816f671b80fd2db99dc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Tue, 19 May 2026 16:20:25 +0100 Subject: [PATCH 2/6] Update composer dependency analyser --- composer-dependency-analyser.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 236115a..9ae1971 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -20,8 +20,10 @@ [ 'friendsofphp/php-cs-fixer', 'phpstan/phpstan', + 'psalm/plugin-symfony', 'rector/rector', 'squizlabs/php_codesniffer', + 'vimeo/psalm', ], [ErrorType::UNUSED_DEPENDENCY] ); From 5f929939188ac0b6554fa2206ba652d023547eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Tue, 19 May 2026 16:20:38 +0100 Subject: [PATCH 3/6] Update gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4c120ba..1f1aa01 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,5 @@ composer.phar .DS_Store *.local -CLAUDE.md .claude .cursor From f1b10fd9bad44c160a9b3694d1958dba6af26c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Tue, 19 May 2026 16:43:23 +0100 Subject: [PATCH 4/6] Fix psalm errors --- .github/workflows/continuous-integration.yml | 6 ++-- .../ArchitectureSniffer.php | 22 +++++++++++---- .../Configuration/ArchitectureLibrary.php | 28 +++++++++++++++---- .../Helper/TypeChecker.php | 14 ++++++++-- Kununu/CsFixer/Command/CsFixerCommand.php | 6 +++- .../CsFixer/Command/CsFixerGitHookCommand.php | 25 +++++++++++++++-- Kununu/CsFixer/CsFixerPlugin.php | 8 ++++-- .../EmptyLineAfterClassElementsSniff.php | 8 +++--- Kununu/Sniffs/Files/LineLengthSniff.php | 3 +- .../MethodSignatureArgumentsSniff.php | 24 ++++++++-------- .../PHP/NoNewLineBeforeDeclareStrictSniff.php | 2 +- dist/psalm.xml.dist | 2 +- 12 files changed, 107 insertions(+), 41 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 031d4d6..693a507 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -52,12 +52,12 @@ jobs: - name: Run PHPStan run: vendor/bin/phpstan analyse - - name: Run Rector - run: vendor/bin/rector process --ansi --dry-run --config rector.php Kununu/ tests/ - - name: Run Psalm run: vendor/bin/psalm --no-cache + - name: Run Rector + run: vendor/bin/rector process --ansi --dry-run --config rector.php Kununu/ tests/ + build: needs: checks name: PHPUnit diff --git a/Kununu/ArchitectureSniffer/ArchitectureSniffer.php b/Kununu/ArchitectureSniffer/ArchitectureSniffer.php index c9ba1d4..efaae0d 100644 --- a/Kununu/ArchitectureSniffer/ArchitectureSniffer.php +++ b/Kununu/ArchitectureSniffer/ArchitectureSniffer.php @@ -69,12 +69,19 @@ public function testArchitecture(): iterable ); } // groups with at least one include from a global namespace other than App\\, the depends_on properties must not be defined + /** @var array> $architecture */ $groupsWithIncludesFromGlobalNamespace = array_filter( $architecture, - static fn(array $group) => !array_filter( - is_array($group[Group::INCLUDES_KEY] ?? null) ? $group[Group::INCLUDES_KEY] : [], - static fn($include) => str_starts_with((string) $include, 'App\\') - ) + static function(array $group): bool { + /** @var array|mixed $includes */ + $includes = $group[Group::INCLUDES_KEY] ?? null; + $includesArray = is_array($includes) ? $includes : []; + + return !array_filter( + $includesArray, + static fn(string $include): bool => str_starts_with($include, 'App\\') + ); + } ); if ($groupsWithIncludesFromGlobalNamespace) { @@ -89,9 +96,12 @@ public function testArchitecture(): iterable } } - $library = new ArchitectureLibrary($architecture); + /** @var array $architectureTyped */ + $architectureTyped = $architecture; + $library = new ArchitectureLibrary($architectureTyped); - foreach (array_keys($architecture) as $groupName) { + /** @var string $groupName */ + foreach (array_keys($architectureTyped) as $groupName) { foreach (RuleBuilder::getRules($library->getGroupBy($groupName), $library) as $rule) { yield $rule; } diff --git a/Kununu/ArchitectureSniffer/Configuration/ArchitectureLibrary.php b/Kununu/ArchitectureSniffer/Configuration/ArchitectureLibrary.php index 8d2bd5b..1f0a5b3 100644 --- a/Kununu/ArchitectureSniffer/Configuration/ArchitectureLibrary.php +++ b/Kununu/ArchitectureSniffer/Configuration/ArchitectureLibrary.php @@ -17,28 +17,44 @@ final class ArchitectureLibrary */ public function __construct(array $groups) { - GroupFlattener::$groups = $groups; + /** @var array> $groupsTyped */ + $groupsTyped = $groups; + GroupFlattener::$groups = $groupsTyped; foreach ($groups as $groupName => $attributes) { + if (!is_array($attributes)) { + throw new InvalidArgumentException( + "Group '$groupName' must be an array." + ); + } + if (!TypeChecker::isArrayOfStrings($attributes[Group::INCLUDES_KEY])) { throw new InvalidArgumentException( "Group '$groupName' includes must be an array of strings." ); } - $flattenedIncludes = GroupFlattener::flattenIncludes($groupName, $attributes[Group::INCLUDES_KEY]); + /** @var string[] $includes */ + $includes = $attributes[Group::INCLUDES_KEY]; + $flattenedIncludes = GroupFlattener::flattenIncludes($groupName, $includes); + + /** @var string[] $excludes */ + $excludes = isset($attributes[Group::EXCLUDES_KEY]) + && TypeChecker::isArrayOfStrings($attributes[Group::EXCLUDES_KEY]) + ? $attributes[Group::EXCLUDES_KEY] + : []; $flattenedExcludes = GroupFlattener::flattenExcludes( groupName: $groupName, - excludes: isset($attributes[Group::EXCLUDES_KEY]) - && TypeChecker::isArrayOfStrings($attributes[Group::EXCLUDES_KEY]) ? - $attributes[Group::EXCLUDES_KEY] : [], + excludes: $excludes, flattenedIncludes: $flattenedIncludes ); + /** @var array $targetAttributes */ + $targetAttributes = $attributes; $this->groups[$groupName] = Group::buildFrom( groupName: $groupName, flattenedIncludes: $flattenedIncludes, - targetAttributes: $attributes, + targetAttributes: $targetAttributes, flattenedExcludes: $flattenedExcludes, ); } diff --git a/Kununu/ArchitectureSniffer/Helper/TypeChecker.php b/Kununu/ArchitectureSniffer/Helper/TypeChecker.php index 6f5956e..13cb161 100644 --- a/Kununu/ArchitectureSniffer/Helper/TypeChecker.php +++ b/Kununu/ArchitectureSniffer/Helper/TypeChecker.php @@ -39,13 +39,23 @@ public static function isArrayOfStrings(mixed $arr): bool /** * @return string[] + * + * @psalm-return list */ public static function castArrayOfStrings(mixed $arrayOfStrings): array { - if (self::isArrayOfStrings($arrayOfStrings) === false) { + if (!is_array($arrayOfStrings)) { throw new InvalidArgumentException('Input must be an array of strings.'); } - return $arrayOfStrings; + $result = []; + foreach ($arrayOfStrings as $item) { + if (!is_string($item)) { + throw new InvalidArgumentException('Input must be an array of strings.'); + } + $result[] = $item; + } + + return $result; } } diff --git a/Kununu/CsFixer/Command/CsFixerCommand.php b/Kununu/CsFixer/Command/CsFixerCommand.php index 178788f..23185fd 100644 --- a/Kununu/CsFixer/Command/CsFixerCommand.php +++ b/Kununu/CsFixer/Command/CsFixerCommand.php @@ -76,7 +76,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::FAILURE; } - $configSource = $input->getOption(self::OPTION_CONFIG) ?: __DIR__ . '/../../../php-cs-fixer.php'; + $configOption = $input->getOption(self::OPTION_CONFIG); + $configSource = is_string($configOption) && $configOption !== '' + ? $configOption + : __DIR__ . '/../../../php-cs-fixer.php'; $configPath = realpath($configSource); if ($configPath === false || !is_file($configPath)) { @@ -124,6 +127,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function getVendorDir(): ?string { try { + /** @var mixed $vendorDir */ $vendorDir = $this->requireComposer()->getConfig()->get('vendor-dir'); if (is_string($vendorDir) && is_dir($vendorDir)) { diff --git a/Kununu/CsFixer/Command/CsFixerGitHookCommand.php b/Kununu/CsFixer/Command/CsFixerGitHookCommand.php index 2ef8db3..f0d90f8 100644 --- a/Kununu/CsFixer/Command/CsFixerGitHookCommand.php +++ b/Kununu/CsFixer/Command/CsFixerGitHookCommand.php @@ -113,9 +113,10 @@ private function installHook(string $gitPath): void private function linkConfigAndBinary(string $gitPath): void { $vendorDir = $this->resolveVendorDir($gitPath); + $codeToolsDir = $this->resolveCodeToolsDir($vendorDir); $this->ensureSymlinkRelative( - $vendorDir . '/kununu/code-tools/php-cs-fixer.php', + $codeToolsDir . '/php-cs-fixer.php', $gitPath . '/kununu/.php-cs-fixer.php' ); @@ -125,6 +126,24 @@ private function linkConfigAndBinary(string $gitPath): void ); } + private function resolveCodeToolsDir(string $vendorDir): string + { + // Standard installation: code-tools is in vendor/kununu/code-tools + $vendorPath = $vendorDir . '/kununu/code-tools'; + if (is_dir($vendorPath) && file_exists($vendorPath . '/php-cs-fixer.php')) { + return $vendorPath; + } + + // Self-installation: running in the code-tools repo itself + // The config file is at the repo root (vendor's parent directory) + $repoRoot = dirname($vendorDir); + if (file_exists($repoRoot . '/php-cs-fixer.php')) { + return $repoRoot; + } + + throw new RuntimeException('Could not locate code-tools package.'); + } + private function resolveVendorDir(string $rootGitPath): string { $repoRoot = basename($rootGitPath) === '.git' ? dirname($rootGitPath) : $rootGitPath; @@ -140,7 +159,9 @@ private function resolveVendorDir(string $rootGitPath): string foreach ($candidates as $candidate) { if (is_dir($candidate)) { - return realpath($candidate) ?: $candidate; + $realCandidate = realpath($candidate); + + return $realCandidate !== false ? $realCandidate : $candidate; } } diff --git a/Kununu/CsFixer/CsFixerPlugin.php b/Kununu/CsFixer/CsFixerPlugin.php index 5678c95..18bee40 100644 --- a/Kununu/CsFixer/CsFixerPlugin.php +++ b/Kununu/CsFixer/CsFixerPlugin.php @@ -19,8 +19,8 @@ final class CsFixerPlugin implements PluginInterface, EventSubscriberInterface, Capable { - private Composer $composer; - private IOInterface $io; + private ?Composer $composer = null; + private ?IOInterface $io = null; public static function getSubscribedEvents(): array { @@ -54,6 +54,10 @@ public function getCapabilities(): array /** @throws ExceptionInterface */ public function addCsFixerGitHooks(): void { + if ($this->composer === null || $this->io === null) { + return; + } + $command = new CsFixerGitHookCommand(); $command->setComposer($this->composer); $command->setIO($this->io); diff --git a/Kununu/Sniffs/Classes/EmptyLineAfterClassElementsSniff.php b/Kununu/Sniffs/Classes/EmptyLineAfterClassElementsSniff.php index 1cf7907..50d2f9c 100644 --- a/Kununu/Sniffs/Classes/EmptyLineAfterClassElementsSniff.php +++ b/Kununu/Sniffs/Classes/EmptyLineAfterClassElementsSniff.php @@ -6,7 +6,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -class EmptyLineAfterClassElementsSniff implements Sniff +final class EmptyLineAfterClassElementsSniff implements Sniff { public function register(): array { @@ -25,7 +25,7 @@ public function process(File $phpcsFile, $stackPtr): void $this->checkProperties($phpcsFile, $classOpen, $classClose); } - protected function checkProperties(File $phpcsFile, int $scopeStart, int $scopeEnd): void + private function checkProperties(File $phpcsFile, int $scopeStart, int $scopeEnd): void { $tokens = $phpcsFile->getTokens(); $lastProperty = null; @@ -115,7 +115,7 @@ protected function checkProperties(File $phpcsFile, int $scopeStart, int $scopeE } } - protected function checkLastElement( + private function checkLastElement( File $phpcsFile, int $scopeStart, int $scopeEnd, @@ -181,7 +181,7 @@ protected function checkLastElement( /** * @param array> $tokens */ - protected function fix(File $phpcsFile, mixed $found, int $semicolon, int $nextContent, array $tokens): void + private function fix(File $phpcsFile, mixed $found, int $semicolon, int $nextContent, array $tokens): void { $phpcsFile->fixer->beginChangeset(); if ($found > 1) { diff --git a/Kununu/Sniffs/Files/LineLengthSniff.php b/Kununu/Sniffs/Files/LineLengthSniff.php index d28a0ea..98b9409 100644 --- a/Kununu/Sniffs/Files/LineLengthSniff.php +++ b/Kununu/Sniffs/Files/LineLengthSniff.php @@ -6,7 +6,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff as PHP_CodeSnifferLineLengthSniff; -class LineLengthSniff extends PHP_CodeSnifferLineLengthSniff +final class LineLengthSniff extends PHP_CodeSnifferLineLengthSniff { public $lineLimit = 100; public $absoluteLineLimit = 120; @@ -14,6 +14,7 @@ class LineLengthSniff extends PHP_CodeSnifferLineLengthSniff public function process(File $phpcsFile, $stackPtr): int { + /** @var array $tokens */ $tokens = $phpcsFile->getTokens(); for ($i = 1; $i < $phpcsFile->numTokens; ++$i) { if ($tokens[$i]['column'] === 1) { diff --git a/Kununu/Sniffs/Formatting/MethodSignatureArgumentsSniff.php b/Kununu/Sniffs/Formatting/MethodSignatureArgumentsSniff.php index a76423c..411ae25 100644 --- a/Kununu/Sniffs/Formatting/MethodSignatureArgumentsSniff.php +++ b/Kununu/Sniffs/Formatting/MethodSignatureArgumentsSniff.php @@ -12,7 +12,7 @@ * * Prevent the usage of multiline for short method signatures and single lines for long ones. */ -class MethodSignatureArgumentsSniff implements Sniff +final class MethodSignatureArgumentsSniff implements Sniff { public int $methodSignatureLengthHardBreak = 120; @@ -90,7 +90,7 @@ public function process(File $phpcsFile, $stackPtr): void $this->makeMethodSignatureSingleLine($phpcsFile, $stackPtr); } - protected function makeMethodSignatureSingleLine(File $phpcsFile, int $stackPtr): void + private function makeMethodSignatureSingleLine(File $phpcsFile, int $stackPtr): void { $tokens = $phpcsFile->getTokens(); @@ -123,7 +123,7 @@ protected function makeMethodSignatureSingleLine(File $phpcsFile, int $stackPtr) $phpcsFile->fixer->endChangeset(); } - protected function makeMethodSignatureMultiline(File $phpcsFile, int $stackPtr): void + private function makeMethodSignatureMultiline(File $phpcsFile, int $stackPtr): void { $tokens = $phpcsFile->getTokens(); $openParenthesisPosition = $tokens[$stackPtr]['parenthesis_opener']; @@ -155,7 +155,7 @@ protected function makeMethodSignatureMultiline(File $phpcsFile, int $stackPtr): $phpcsFile->fixer->endChangeset(); } - protected function removeEverythingBetweenPositions(File $phpcsFile, int $fromPosition, int $toPosition): void + private function removeEverythingBetweenPositions(File $phpcsFile, int $fromPosition, int $toPosition): void { for ($i = $fromPosition + 1; $i < $toPosition; ++$i) { $phpcsFile->fixer->replaceToken($i, ''); @@ -165,7 +165,7 @@ protected function removeEverythingBetweenPositions(File $phpcsFile, int $fromPo /** * @param array> $tokens */ - protected function areTokensOnTheSameLine(array $tokens, int $firstPosition, int $secondPosition): bool + private function areTokensOnTheSameLine(array $tokens, int $firstPosition, int $secondPosition): bool { return $tokens[$firstPosition]['line'] === $tokens[$secondPosition]['line']; } @@ -173,7 +173,7 @@ protected function areTokensOnTheSameLine(array $tokens, int $firstPosition, int /** * @throws DeepExitException */ - protected function getMethodSignatureLength(File $phpcsFile, int $stackPtr): int + private function getMethodSignatureLength(File $phpcsFile, int $stackPtr): int { $tokens = $phpcsFile->getTokens(); if ($tokens[$stackPtr]['code'] !== T_FUNCTION) { @@ -191,7 +191,7 @@ protected function getMethodSignatureLength(File $phpcsFile, int $stackPtr): int return $this->getMethodSignatureMultilineLength($tokens, $stackPtr, $methodProperties, $methodParameters); } - protected function getIndentationWhitespace(File $phpcsFile, int $prevIndex): string + private function getIndentationWhitespace(File $phpcsFile, int $prevIndex): string { $tokens = $phpcsFile->getTokens(); @@ -209,7 +209,7 @@ protected function getIndentationWhitespace(File $phpcsFile, int $prevIndex): st /** * @param array> $tokens */ - protected function getLineEndingPosition(array $tokens, int $position): int + private function getLineEndingPosition(array $tokens, int $position): int { while (!empty($tokens[$position]) && !str_contains((string) $tokens[$position]['content'], PHP_EOL)) { ++$position; @@ -221,7 +221,7 @@ protected function getLineEndingPosition(array $tokens, int $position): int /** * @param array> $tokens */ - protected function getMethodSingleLineSignatureLength(array $tokens, int $stackPtr): int + private function getMethodSingleLineSignatureLength(array $tokens, int $stackPtr): int { $position = $this->getLineEndingPosition($tokens, $stackPtr); @@ -231,7 +231,7 @@ protected function getMethodSingleLineSignatureLength(array $tokens, int $stackP /** * @param array> $tokens */ - protected function getFirstTokenOfLine(array $tokens, int $index): int + private function getFirstTokenOfLine(array $tokens, int $index): int { $line = $tokens[$index]['line']; @@ -248,7 +248,7 @@ protected function getFirstTokenOfLine(array $tokens, int $index): int * @param array $methodProperties * @param array> $methodParameters */ - protected function getMethodSignatureMultilineLength( + private function getMethodSignatureMultilineLength( array $tokens, int $stackPtr, array $methodProperties, @@ -283,7 +283,7 @@ protected function getMethodSignatureMultilineLength( /** * @param array $methodParameter */ - protected function getParameterTotalLength(array $methodParameter): int + private function getParameterTotalLength(array $methodParameter): int { $length = 0; $length += mb_strlen((string) $methodParameter['content']); diff --git a/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php b/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php index 1926253..d1bf59f 100644 --- a/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php +++ b/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php @@ -9,7 +9,7 @@ /** * Prevents empty new line before declare(strict_types=1). */ -class NoNewLineBeforeDeclareStrictSniff implements Sniff +final class NoNewLineBeforeDeclareStrictSniff implements Sniff { public function register(): array { diff --git a/dist/psalm.xml.dist b/dist/psalm.xml.dist index da88aa4..cabd62e 100644 --- a/dist/psalm.xml.dist +++ b/dist/psalm.xml.dist @@ -1,6 +1,6 @@ Date: Tue, 19 May 2026 16:46:37 +0100 Subject: [PATCH 5/6] Fix phpstan errors --- Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php b/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php index d1bf59f..7ad2a2f 100644 --- a/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php +++ b/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php @@ -16,7 +16,7 @@ public function register(): array return [T_OPEN_TAG]; } - public function process(File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr): int { $tokens = $phpcsFile->getTokens(); $declare = $phpcsFile->findNext(T_DECLARE, $stackPtr + 1); From 0fa2a983546dabc7863922b3d70b735165efb01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Wed, 20 May 2026 09:07:48 +0100 Subject: [PATCH 6/6] Update level errors and add exclusions --- .../ArchitectureSniffer.php | 22 ++++----------- .../Configuration/ArchitectureLibrary.php | 28 ++++--------------- .../Helper/TypeChecker.php | 22 ++------------- Kununu/CsFixer/Command/CsFixerCommand.php | 6 +--- .../CsFixer/Command/CsFixerGitHookCommand.php | 25 ++--------------- .../EmptyLineAfterClassElementsSniff.php | 8 +++--- Kununu/Sniffs/Files/LineLengthSniff.php | 3 +- .../MethodSignatureArgumentsSniff.php | 24 ++++++++-------- .../PHP/NoNewLineBeforeDeclareStrictSniff.php | 4 +-- dist/psalm.xml.dist | 2 +- psalm.xml | 10 ++++++- 11 files changed, 48 insertions(+), 106 deletions(-) diff --git a/Kununu/ArchitectureSniffer/ArchitectureSniffer.php b/Kununu/ArchitectureSniffer/ArchitectureSniffer.php index efaae0d..e370a06 100644 --- a/Kununu/ArchitectureSniffer/ArchitectureSniffer.php +++ b/Kununu/ArchitectureSniffer/ArchitectureSniffer.php @@ -69,19 +69,12 @@ public function testArchitecture(): iterable ); } // groups with at least one include from a global namespace other than App\\, the depends_on properties must not be defined - /** @var array> $architecture */ $groupsWithIncludesFromGlobalNamespace = array_filter( $architecture, - static function(array $group): bool { - /** @var array|mixed $includes */ - $includes = $group[Group::INCLUDES_KEY] ?? null; - $includesArray = is_array($includes) ? $includes : []; - - return !array_filter( - $includesArray, - static fn(string $include): bool => str_starts_with($include, 'App\\') - ); - } + static fn(array $group) => !array_filter( + is_array($group[Group::INCLUDES_KEY] ?? null) ? $group[Group::INCLUDES_KEY] : [], + static fn(mixed $include): bool => str_starts_with((string) $include, 'App\\') + ) ); if ($groupsWithIncludesFromGlobalNamespace) { @@ -96,12 +89,9 @@ static function(array $group): bool { } } - /** @var array $architectureTyped */ - $architectureTyped = $architecture; - $library = new ArchitectureLibrary($architectureTyped); + $library = new ArchitectureLibrary($architecture); - /** @var string $groupName */ - foreach (array_keys($architectureTyped) as $groupName) { + foreach (array_keys($architecture) as $groupName) { foreach (RuleBuilder::getRules($library->getGroupBy($groupName), $library) as $rule) { yield $rule; } diff --git a/Kununu/ArchitectureSniffer/Configuration/ArchitectureLibrary.php b/Kununu/ArchitectureSniffer/Configuration/ArchitectureLibrary.php index 1f0a5b3..8d2bd5b 100644 --- a/Kununu/ArchitectureSniffer/Configuration/ArchitectureLibrary.php +++ b/Kununu/ArchitectureSniffer/Configuration/ArchitectureLibrary.php @@ -17,44 +17,28 @@ final class ArchitectureLibrary */ public function __construct(array $groups) { - /** @var array> $groupsTyped */ - $groupsTyped = $groups; - GroupFlattener::$groups = $groupsTyped; + GroupFlattener::$groups = $groups; foreach ($groups as $groupName => $attributes) { - if (!is_array($attributes)) { - throw new InvalidArgumentException( - "Group '$groupName' must be an array." - ); - } - if (!TypeChecker::isArrayOfStrings($attributes[Group::INCLUDES_KEY])) { throw new InvalidArgumentException( "Group '$groupName' includes must be an array of strings." ); } - /** @var string[] $includes */ - $includes = $attributes[Group::INCLUDES_KEY]; - $flattenedIncludes = GroupFlattener::flattenIncludes($groupName, $includes); - - /** @var string[] $excludes */ - $excludes = isset($attributes[Group::EXCLUDES_KEY]) - && TypeChecker::isArrayOfStrings($attributes[Group::EXCLUDES_KEY]) - ? $attributes[Group::EXCLUDES_KEY] - : []; + $flattenedIncludes = GroupFlattener::flattenIncludes($groupName, $attributes[Group::INCLUDES_KEY]); $flattenedExcludes = GroupFlattener::flattenExcludes( groupName: $groupName, - excludes: $excludes, + excludes: isset($attributes[Group::EXCLUDES_KEY]) + && TypeChecker::isArrayOfStrings($attributes[Group::EXCLUDES_KEY]) ? + $attributes[Group::EXCLUDES_KEY] : [], flattenedIncludes: $flattenedIncludes ); - /** @var array $targetAttributes */ - $targetAttributes = $attributes; $this->groups[$groupName] = Group::buildFrom( groupName: $groupName, flattenedIncludes: $flattenedIncludes, - targetAttributes: $targetAttributes, + targetAttributes: $attributes, flattenedExcludes: $flattenedExcludes, ); } diff --git a/Kununu/ArchitectureSniffer/Helper/TypeChecker.php b/Kununu/ArchitectureSniffer/Helper/TypeChecker.php index 13cb161..677e5a8 100644 --- a/Kununu/ArchitectureSniffer/Helper/TypeChecker.php +++ b/Kununu/ArchitectureSniffer/Helper/TypeChecker.php @@ -13,13 +13,7 @@ public static function isArrayKeysOfStrings(mixed $arr): bool return false; } - foreach (array_keys($arr) as $key) { - if (!is_string($key)) { - return false; - } - } - - return true; + return array_all(array_keys($arr), static fn($key) => is_string($key)); } public static function isArrayOfStrings(mixed $arr): bool @@ -39,23 +33,13 @@ public static function isArrayOfStrings(mixed $arr): bool /** * @return string[] - * - * @psalm-return list */ public static function castArrayOfStrings(mixed $arrayOfStrings): array { - if (!is_array($arrayOfStrings)) { + if (self::isArrayOfStrings($arrayOfStrings) === false) { throw new InvalidArgumentException('Input must be an array of strings.'); } - $result = []; - foreach ($arrayOfStrings as $item) { - if (!is_string($item)) { - throw new InvalidArgumentException('Input must be an array of strings.'); - } - $result[] = $item; - } - - return $result; + return $arrayOfStrings; } } diff --git a/Kununu/CsFixer/Command/CsFixerCommand.php b/Kununu/CsFixer/Command/CsFixerCommand.php index 23185fd..5e0fded 100644 --- a/Kununu/CsFixer/Command/CsFixerCommand.php +++ b/Kununu/CsFixer/Command/CsFixerCommand.php @@ -76,10 +76,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::FAILURE; } - $configOption = $input->getOption(self::OPTION_CONFIG); - $configSource = is_string($configOption) && $configOption !== '' - ? $configOption - : __DIR__ . '/../../../php-cs-fixer.php'; + $configSource = $input->getOption(self::OPTION_CONFIG) ?? __DIR__ . '/../../../php-cs-fixer.php'; $configPath = realpath($configSource); if ($configPath === false || !is_file($configPath)) { @@ -127,7 +124,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function getVendorDir(): ?string { try { - /** @var mixed $vendorDir */ $vendorDir = $this->requireComposer()->getConfig()->get('vendor-dir'); if (is_string($vendorDir) && is_dir($vendorDir)) { diff --git a/Kununu/CsFixer/Command/CsFixerGitHookCommand.php b/Kununu/CsFixer/Command/CsFixerGitHookCommand.php index f0d90f8..e622566 100644 --- a/Kununu/CsFixer/Command/CsFixerGitHookCommand.php +++ b/Kununu/CsFixer/Command/CsFixerGitHookCommand.php @@ -113,10 +113,9 @@ private function installHook(string $gitPath): void private function linkConfigAndBinary(string $gitPath): void { $vendorDir = $this->resolveVendorDir($gitPath); - $codeToolsDir = $this->resolveCodeToolsDir($vendorDir); $this->ensureSymlinkRelative( - $codeToolsDir . '/php-cs-fixer.php', + $vendorDir . '/kununu/code-tools/php-cs-fixer.php', $gitPath . '/kununu/.php-cs-fixer.php' ); @@ -126,24 +125,6 @@ private function linkConfigAndBinary(string $gitPath): void ); } - private function resolveCodeToolsDir(string $vendorDir): string - { - // Standard installation: code-tools is in vendor/kununu/code-tools - $vendorPath = $vendorDir . '/kununu/code-tools'; - if (is_dir($vendorPath) && file_exists($vendorPath . '/php-cs-fixer.php')) { - return $vendorPath; - } - - // Self-installation: running in the code-tools repo itself - // The config file is at the repo root (vendor's parent directory) - $repoRoot = dirname($vendorDir); - if (file_exists($repoRoot . '/php-cs-fixer.php')) { - return $repoRoot; - } - - throw new RuntimeException('Could not locate code-tools package.'); - } - private function resolveVendorDir(string $rootGitPath): string { $repoRoot = basename($rootGitPath) === '.git' ? dirname($rootGitPath) : $rootGitPath; @@ -159,9 +140,9 @@ private function resolveVendorDir(string $rootGitPath): string foreach ($candidates as $candidate) { if (is_dir($candidate)) { - $realCandidate = realpath($candidate); + $resolved = realpath($candidate); - return $realCandidate !== false ? $realCandidate : $candidate; + return $resolved !== false ? $resolved : $candidate; } } diff --git a/Kununu/Sniffs/Classes/EmptyLineAfterClassElementsSniff.php b/Kununu/Sniffs/Classes/EmptyLineAfterClassElementsSniff.php index 50d2f9c..1cf7907 100644 --- a/Kununu/Sniffs/Classes/EmptyLineAfterClassElementsSniff.php +++ b/Kununu/Sniffs/Classes/EmptyLineAfterClassElementsSniff.php @@ -6,7 +6,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -final class EmptyLineAfterClassElementsSniff implements Sniff +class EmptyLineAfterClassElementsSniff implements Sniff { public function register(): array { @@ -25,7 +25,7 @@ public function process(File $phpcsFile, $stackPtr): void $this->checkProperties($phpcsFile, $classOpen, $classClose); } - private function checkProperties(File $phpcsFile, int $scopeStart, int $scopeEnd): void + protected function checkProperties(File $phpcsFile, int $scopeStart, int $scopeEnd): void { $tokens = $phpcsFile->getTokens(); $lastProperty = null; @@ -115,7 +115,7 @@ private function checkProperties(File $phpcsFile, int $scopeStart, int $scopeEnd } } - private function checkLastElement( + protected function checkLastElement( File $phpcsFile, int $scopeStart, int $scopeEnd, @@ -181,7 +181,7 @@ private function checkLastElement( /** * @param array> $tokens */ - private function fix(File $phpcsFile, mixed $found, int $semicolon, int $nextContent, array $tokens): void + protected function fix(File $phpcsFile, mixed $found, int $semicolon, int $nextContent, array $tokens): void { $phpcsFile->fixer->beginChangeset(); if ($found > 1) { diff --git a/Kununu/Sniffs/Files/LineLengthSniff.php b/Kununu/Sniffs/Files/LineLengthSniff.php index 98b9409..d28a0ea 100644 --- a/Kununu/Sniffs/Files/LineLengthSniff.php +++ b/Kununu/Sniffs/Files/LineLengthSniff.php @@ -6,7 +6,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff as PHP_CodeSnifferLineLengthSniff; -final class LineLengthSniff extends PHP_CodeSnifferLineLengthSniff +class LineLengthSniff extends PHP_CodeSnifferLineLengthSniff { public $lineLimit = 100; public $absoluteLineLimit = 120; @@ -14,7 +14,6 @@ final class LineLengthSniff extends PHP_CodeSnifferLineLengthSniff public function process(File $phpcsFile, $stackPtr): int { - /** @var array $tokens */ $tokens = $phpcsFile->getTokens(); for ($i = 1; $i < $phpcsFile->numTokens; ++$i) { if ($tokens[$i]['column'] === 1) { diff --git a/Kununu/Sniffs/Formatting/MethodSignatureArgumentsSniff.php b/Kununu/Sniffs/Formatting/MethodSignatureArgumentsSniff.php index 411ae25..a76423c 100644 --- a/Kununu/Sniffs/Formatting/MethodSignatureArgumentsSniff.php +++ b/Kununu/Sniffs/Formatting/MethodSignatureArgumentsSniff.php @@ -12,7 +12,7 @@ * * Prevent the usage of multiline for short method signatures and single lines for long ones. */ -final class MethodSignatureArgumentsSniff implements Sniff +class MethodSignatureArgumentsSniff implements Sniff { public int $methodSignatureLengthHardBreak = 120; @@ -90,7 +90,7 @@ public function process(File $phpcsFile, $stackPtr): void $this->makeMethodSignatureSingleLine($phpcsFile, $stackPtr); } - private function makeMethodSignatureSingleLine(File $phpcsFile, int $stackPtr): void + protected function makeMethodSignatureSingleLine(File $phpcsFile, int $stackPtr): void { $tokens = $phpcsFile->getTokens(); @@ -123,7 +123,7 @@ private function makeMethodSignatureSingleLine(File $phpcsFile, int $stackPtr): $phpcsFile->fixer->endChangeset(); } - private function makeMethodSignatureMultiline(File $phpcsFile, int $stackPtr): void + protected function makeMethodSignatureMultiline(File $phpcsFile, int $stackPtr): void { $tokens = $phpcsFile->getTokens(); $openParenthesisPosition = $tokens[$stackPtr]['parenthesis_opener']; @@ -155,7 +155,7 @@ private function makeMethodSignatureMultiline(File $phpcsFile, int $stackPtr): v $phpcsFile->fixer->endChangeset(); } - private function removeEverythingBetweenPositions(File $phpcsFile, int $fromPosition, int $toPosition): void + protected function removeEverythingBetweenPositions(File $phpcsFile, int $fromPosition, int $toPosition): void { for ($i = $fromPosition + 1; $i < $toPosition; ++$i) { $phpcsFile->fixer->replaceToken($i, ''); @@ -165,7 +165,7 @@ private function removeEverythingBetweenPositions(File $phpcsFile, int $fromPosi /** * @param array> $tokens */ - private function areTokensOnTheSameLine(array $tokens, int $firstPosition, int $secondPosition): bool + protected function areTokensOnTheSameLine(array $tokens, int $firstPosition, int $secondPosition): bool { return $tokens[$firstPosition]['line'] === $tokens[$secondPosition]['line']; } @@ -173,7 +173,7 @@ private function areTokensOnTheSameLine(array $tokens, int $firstPosition, int $ /** * @throws DeepExitException */ - private function getMethodSignatureLength(File $phpcsFile, int $stackPtr): int + protected function getMethodSignatureLength(File $phpcsFile, int $stackPtr): int { $tokens = $phpcsFile->getTokens(); if ($tokens[$stackPtr]['code'] !== T_FUNCTION) { @@ -191,7 +191,7 @@ private function getMethodSignatureLength(File $phpcsFile, int $stackPtr): int return $this->getMethodSignatureMultilineLength($tokens, $stackPtr, $methodProperties, $methodParameters); } - private function getIndentationWhitespace(File $phpcsFile, int $prevIndex): string + protected function getIndentationWhitespace(File $phpcsFile, int $prevIndex): string { $tokens = $phpcsFile->getTokens(); @@ -209,7 +209,7 @@ private function getIndentationWhitespace(File $phpcsFile, int $prevIndex): stri /** * @param array> $tokens */ - private function getLineEndingPosition(array $tokens, int $position): int + protected function getLineEndingPosition(array $tokens, int $position): int { while (!empty($tokens[$position]) && !str_contains((string) $tokens[$position]['content'], PHP_EOL)) { ++$position; @@ -221,7 +221,7 @@ private function getLineEndingPosition(array $tokens, int $position): int /** * @param array> $tokens */ - private function getMethodSingleLineSignatureLength(array $tokens, int $stackPtr): int + protected function getMethodSingleLineSignatureLength(array $tokens, int $stackPtr): int { $position = $this->getLineEndingPosition($tokens, $stackPtr); @@ -231,7 +231,7 @@ private function getMethodSingleLineSignatureLength(array $tokens, int $stackPtr /** * @param array> $tokens */ - private function getFirstTokenOfLine(array $tokens, int $index): int + protected function getFirstTokenOfLine(array $tokens, int $index): int { $line = $tokens[$index]['line']; @@ -248,7 +248,7 @@ private function getFirstTokenOfLine(array $tokens, int $index): int * @param array $methodProperties * @param array> $methodParameters */ - private function getMethodSignatureMultilineLength( + protected function getMethodSignatureMultilineLength( array $tokens, int $stackPtr, array $methodProperties, @@ -283,7 +283,7 @@ private function getMethodSignatureMultilineLength( /** * @param array $methodParameter */ - private function getParameterTotalLength(array $methodParameter): int + protected function getParameterTotalLength(array $methodParameter): int { $length = 0; $length += mb_strlen((string) $methodParameter['content']); diff --git a/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php b/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php index 7ad2a2f..1926253 100644 --- a/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php +++ b/Kununu/Sniffs/PHP/NoNewLineBeforeDeclareStrictSniff.php @@ -9,14 +9,14 @@ /** * Prevents empty new line before declare(strict_types=1). */ -final class NoNewLineBeforeDeclareStrictSniff implements Sniff +class NoNewLineBeforeDeclareStrictSniff implements Sniff { public function register(): array { return [T_OPEN_TAG]; } - public function process(File $phpcsFile, $stackPtr): int + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $declare = $phpcsFile->findNext(T_DECLARE, $stackPtr + 1); diff --git a/dist/psalm.xml.dist b/dist/psalm.xml.dist index cabd62e..cba18c8 100644 --- a/dist/psalm.xml.dist +++ b/dist/psalm.xml.dist @@ -1,6 +1,6 @@ + + + + + + + +