From cf70a098ff564cb3cdb8a9dcf61bfa46b5c33792 Mon Sep 17 00:00:00 2001 From: OhMyFelix Date: Mon, 23 Feb 2026 07:49:39 +0000 Subject: [PATCH 1/7] Tests: cover issue fixtures and add 8.5/next sets --- tests/Cases/IssueTest.php | 72 ++ tests/Cases/RulesetTest.php | 18 +- tests/Cases/SniffTest.php | 18 +- tests/Sets/8.5/Fixtures/Clean.php | 17 + tests/Sets/8.5/Fixtures/DummyClass.php | 8 + tests/Sets/8.5/Fixtures/arrays.php | 20 + tests/Sets/8.5/Fixtures/classes.php | 41 + tests/Sets/8.5/Fixtures/comments.php | 49 ++ .../Sets/8.5/Fixtures/control-structures.php | 46 ++ tests/Sets/8.5/Fixtures/functions.php | 48 ++ tests/Sets/8.5/Fixtures/namespaces.php | 29 + tests/Sets/8.5/Fixtures/operators.php | 41 + tests/Sets/8.5/Fixtures/types.php | 33 + tests/Sets/8.5/ruleset.xml | 4 + tests/Sets/8.5/snapshot.json | 759 ++++++++++++++++++ tests/Sets/next/Fixtures/Clean.php | 17 + tests/Sets/next/Fixtures/DummyClass.php | 8 + tests/Sets/next/Fixtures/arrays.php | 20 + tests/Sets/next/Fixtures/classes.php | 41 + tests/Sets/next/Fixtures/comments.php | 49 ++ .../Sets/next/Fixtures/control-structures.php | 46 ++ tests/Sets/next/Fixtures/functions.php | 48 ++ tests/Sets/next/Fixtures/namespaces.php | 29 + tests/Sets/next/Fixtures/operators.php | 41 + tests/Sets/next/Fixtures/types.php | 33 + tests/Sets/next/ruleset.xml | 4 + tests/Sets/next/snapshot.json | 759 ++++++++++++++++++ 27 files changed, 2296 insertions(+), 2 deletions(-) create mode 100644 tests/Cases/IssueTest.php create mode 100644 tests/Sets/8.5/Fixtures/Clean.php create mode 100644 tests/Sets/8.5/Fixtures/DummyClass.php create mode 100644 tests/Sets/8.5/Fixtures/arrays.php create mode 100644 tests/Sets/8.5/Fixtures/classes.php create mode 100644 tests/Sets/8.5/Fixtures/comments.php create mode 100644 tests/Sets/8.5/Fixtures/control-structures.php create mode 100644 tests/Sets/8.5/Fixtures/functions.php create mode 100644 tests/Sets/8.5/Fixtures/namespaces.php create mode 100644 tests/Sets/8.5/Fixtures/operators.php create mode 100644 tests/Sets/8.5/Fixtures/types.php create mode 100644 tests/Sets/8.5/ruleset.xml create mode 100644 tests/Sets/8.5/snapshot.json create mode 100644 tests/Sets/next/Fixtures/Clean.php create mode 100644 tests/Sets/next/Fixtures/DummyClass.php create mode 100644 tests/Sets/next/Fixtures/arrays.php create mode 100644 tests/Sets/next/Fixtures/classes.php create mode 100644 tests/Sets/next/Fixtures/comments.php create mode 100644 tests/Sets/next/Fixtures/control-structures.php create mode 100644 tests/Sets/next/Fixtures/functions.php create mode 100644 tests/Sets/next/Fixtures/namespaces.php create mode 100644 tests/Sets/next/Fixtures/operators.php create mode 100644 tests/Sets/next/Fixtures/types.php create mode 100644 tests/Sets/next/ruleset.xml create mode 100644 tests/Sets/next/snapshot.json diff --git a/tests/Cases/IssueTest.php b/tests/Cases/IssueTest.php new file mode 100644 index 0000000..8b18841 --- /dev/null +++ b/tests/Cases/IssueTest.php @@ -0,0 +1,72 @@ +setWorkingDirectory(__DIR__ . '/../../'); + $process->run(); + self::assertSame(0, $process->getExitCode(), $process->getErrorOutput()); + + try { + $output = json_decode(trim($process->getOutput()), true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + self::fail('Failed to decode phpcs JSON output: ' . $e->getMessage()); + } + + self::assertIsArray($output); + + $actual = Codesniffer::normalize($output); + $expected = json_decode(file_get_contents($snapshot), true); + + self::assertEquals($expected, $actual); + } + + public static function provideIssues(): Generator + { + $issuesDir = __DIR__ . '/../Issue'; + + if (!is_dir($issuesDir)) { + return; + } + + foreach (Finder::findDirectories('*')->in($issuesDir) as $issueDir) { + foreach (Finder::findFiles('*.php')->in($issueDir->getPathname()) as $phpFile) { + $baseName = $phpFile->getBasename('.php'); + $snapshotFile = $issueDir->getPathname() . '/' . $baseName . '.snapshot.json'; + $rulesetFile = $issueDir->getPathname() . '/' . $baseName . '.ruleset.xml'; + + if (file_exists($snapshotFile) && file_exists($rulesetFile)) { + $key = $issueDir->getBasename() . '/' . $baseName; + yield $key => [$phpFile->getPathname(), $rulesetFile, $snapshotFile]; + } + } + } + } + +} diff --git a/tests/Cases/RulesetTest.php b/tests/Cases/RulesetTest.php index ee6d22c..6da79a5 100644 --- a/tests/Cases/RulesetTest.php +++ b/tests/Cases/RulesetTest.php @@ -3,6 +3,7 @@ namespace Tests\Cases; use Generator; +use JsonException; use Nette\Utils\Finder; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -18,14 +19,29 @@ public function testRuleset(string $folder, string $snapshot): void $process = new Process([ 'vendor/bin/phpcs', '--standard=' . $folder . '/ruleset.xml', + '--runtime-set', + 'ignore_errors_on_exit', + '1', + '--runtime-set', + 'ignore_warnings_on_exit', + '1', '--report=json', '-q', $folder . '/Fixtures', ]); $process->setWorkingDirectory(__DIR__ . '/../../'); $process->run(); + self::assertSame(0, $process->getExitCode(), $process->getErrorOutput()); - $actual = Codesniffer::normalize(json_decode(trim($process->getOutput()), true) ?? []); + try { + $output = json_decode(trim($process->getOutput()), true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + self::fail('Failed to decode phpcs JSON output: ' . $e->getMessage()); + } + + self::assertIsArray($output); + + $actual = Codesniffer::normalize($output); $expected = json_decode(file_get_contents($snapshot), true); self::assertEquals($expected, $actual); diff --git a/tests/Cases/SniffTest.php b/tests/Cases/SniffTest.php index 3b5bcae..7e95ee0 100644 --- a/tests/Cases/SniffTest.php +++ b/tests/Cases/SniffTest.php @@ -3,6 +3,7 @@ namespace Tests\Cases; use Generator; +use JsonException; use Nette\Utils\Finder; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -18,14 +19,29 @@ public function testSniff(string $file, string $ruleset, string $snapshot): void $process = new Process([ 'vendor/bin/phpcs', '--standard=' . $ruleset, + '--runtime-set', + 'ignore_errors_on_exit', + '1', + '--runtime-set', + 'ignore_warnings_on_exit', + '1', '--report=json', '-q', $file, ]); $process->setWorkingDirectory(__DIR__ . '/../../'); $process->run(); + self::assertSame(0, $process->getExitCode(), $process->getErrorOutput()); - $actual = Codesniffer::normalize(json_decode(trim($process->getOutput()), true) ?? []); + try { + $output = json_decode(trim($process->getOutput()), true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + self::fail('Failed to decode phpcs JSON output: ' . $e->getMessage()); + } + + self::assertIsArray($output); + + $actual = Codesniffer::normalize($output); $expected = json_decode(file_get_contents($snapshot), true); self::assertEquals($expected, $actual); diff --git a/tests/Sets/8.5/Fixtures/Clean.php b/tests/Sets/8.5/Fixtures/Clean.php new file mode 100644 index 0000000..11158b2 --- /dev/null +++ b/tests/Sets/8.5/Fixtures/Clean.php @@ -0,0 +1,17 @@ +property ?? 'default'; + } + +} diff --git a/tests/Sets/8.5/Fixtures/DummyClass.php b/tests/Sets/8.5/Fixtures/DummyClass.php new file mode 100644 index 0000000..511f1d4 --- /dev/null +++ b/tests/Sets/8.5/Fixtures/DummyClass.php @@ -0,0 +1,8 @@ +uselessDefault('value'); + + // Reference usage - violation + $arr = [1, 2, 3]; + foreach ($arr as &$item) { + $item *= 2; + } + } + + // Empty function - violation + public function emptyFunction(): void + { + } + + // Useless default value (required param after optional) - violation + public function uselessDefault(string $required, string $optional = 'default'): void + { + } + +} + +// Global function - violation +function globalFunction(): void +{ +} diff --git a/tests/Sets/8.5/Fixtures/namespaces.php b/tests/Sets/8.5/Fixtures/namespaces.php new file mode 100644 index 0000000..c10cd2b --- /dev/null +++ b/tests/Sets/8.5/Fixtures/namespaces.php @@ -0,0 +1,29 @@ + 0 AND $b > 0) { + echo 'both positive'; + } + + if ($a > 0 OR $b > 0) { + echo 'one positive'; + } + + // Numeric literal separator - violation + $bigNumber = 1_000_000; + } + +} diff --git a/tests/Sets/8.5/Fixtures/types.php b/tests/Sets/8.5/Fixtures/types.php new file mode 100644 index 0000000..c9b0e0e --- /dev/null +++ b/tests/Sets/8.5/Fixtures/types.php @@ -0,0 +1,33 @@ + + + + diff --git a/tests/Sets/8.5/snapshot.json b/tests/Sets/8.5/snapshot.json new file mode 100644 index 0000000..dae5eaa --- /dev/null +++ b/tests/Sets/8.5/snapshot.json @@ -0,0 +1,759 @@ +{ + "totals": { + "errors": 97, + "warnings": 2 + }, + "files": { + "Clean.php": { + "errors": 0, + "warnings": 0, + "messages": [] + }, + "DummyClass.php": { + "errors": 0, + "warnings": 0, + "messages": [] + }, + "arrays.php": { + "errors": 9, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 8, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $arr." + }, + { + "line": 8, + "column": 8, + "type": "ERROR", + "source": "Generic.Arrays.DisallowLongArraySyntax.Found", + "message": "Short array syntax must be used to define arrays" + }, + { + "line": 11, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $multiline." + }, + { + "line": 13, + "column": 5, + "type": "ERROR", + "source": "SlevomatCodingStandard.Arrays.TrailingArrayComma.MissingTrailingComma", + "message": "Multi-line arrays must have a trailing comma after the last element." + }, + { + "line": 17, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Arrays.DisallowImplicitArrayCreation.ImplicitArrayCreationUsed", + "message": "Implicit array creation is disallowed." + }, + { + "line": 20, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $spaced." + }, + { + "line": 20, + "column": 11, + "type": "ERROR", + "source": "SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace.SpaceAfterArrayOpen", + "message": "Expected 0 spaces after array opening bracket, 1 found." + }, + { + "line": 20, + "column": 21, + "type": "ERROR", + "source": "SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace.SpaceBeforeArrayClose", + "message": "Expected 0 spaces before array closing bracket, 1 found." + } + ] + }, + "classes.php": { + "errors": 15, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 8, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"ClassesFixture.php\"" + }, + { + "line": 9, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces.NoEmptyLineAfterOpeningBrace", + "message": "There must be one empty line after class opening brace." + }, + { + "line": 11, + "column": 5, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.ClassConstantVisibility.MissingConstantVisibility", + "message": "Constant \\Tests\\Fixtures\\ClassesFixture::FOO visibility missing." + }, + { + "line": 14, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition.DisallowedMultiConstantDefinition", + "message": "Use of multi constant definition is disallowed." + }, + { + "line": 17, + "column": 19, + "type": "ERROR", + "source": "Squiz.Commenting.VariableComment.WrongStyle", + "message": "You must use \"/**\" style comments for a member variable comment" + }, + { + "line": 20, + "column": 5, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition.DisallowedMultiPropertyDefinition", + "message": "Use of multi property definition is disallowed." + }, + { + "line": 20, + "column": 16, + "type": "ERROR", + "source": "PSR2.Classes.PropertyDeclaration.Multiple", + "message": "There must not be more than one property declared per statement" + }, + { + "line": 20, + "column": 16, + "type": "ERROR", + "source": "Squiz.Commenting.VariableComment.WrongStyle", + "message": "You must use \"/**\" style comments for a member variable comment" + }, + { + "line": 23, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 29, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces.NoEmptyLineBeforeClosingBrace", + "message": "There must be one empty line before class closing brace." + }, + { + "line": 32, + "column": 1, + "type": "ERROR", + "source": "PSR1.Classes.ClassDeclaration.MultipleClasses", + "message": "Each class must be in a file by itself" + }, + { + "line": 32, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match interface name; expected file name \"IWrongNaming.php\"" + }, + { + "line": 38, + "column": 1, + "type": "ERROR", + "source": "PSR1.Classes.ClassDeclaration.MultipleClasses", + "message": "Each interface must be in a file by itself" + }, + { + "line": 38, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match trait name; expected file name \"TWrongNaming.php\"" + } + ] + }, + "comments.php": { + "errors": 10, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 8, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @author is forbidden." + }, + { + "line": 9, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @copyright is forbidden." + }, + { + "line": 10, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @package is forbidden." + }, + { + "line": 11, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @since is forbidden." + }, + { + "line": 12, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @todo is forbidden." + }, + { + "line": 14, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"CommentsFixture.php\"" + }, + { + "line": 24, + "column": 8, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenComments.CommentForbidden", + "message": "Documentation comment contains forbidden comment \"Constructor.\"." + }, + { + "line": 27, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + }, + { + "line": 32, + "column": 8, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenComments.CommentForbidden", + "message": "Documentation comment contains forbidden comment \"Name getter.\"." + } + ] + }, + "control-structures.php": { + "errors": 11, + "warnings": 1, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 7, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"ControlStructuresFixture.php\"" + }, + { + "line": 16, + "column": 15, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.DisallowYodaComparison.DisallowedYodaComparison", + "message": "Yoda comparisons are disallowed." + }, + { + "line": 23, + "column": 11, + "type": "ERROR", + "source": "PSR2.ControlStructures.ElseIfDeclaration.NotAllowed", + "message": "Usage of ELSE IF is discouraged; use ELSEIF instead" + }, + { + "line": 28, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $c." + }, + { + "line": 28, + "column": 26, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator.NullCoalesceOperatorNotUsed", + "message": "Use null coalesce operator instead of ternary operator." + }, + { + "line": 31, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $d." + }, + { + "line": 31, + "column": 23, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.UselessTernaryOperator.UselessTernaryOperator", + "message": "Useless ternary operator." + }, + { + "line": 36, + "column": 17, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch.DisallowedContinueWithoutIntegerOperandInSwitch", + "message": "Usage of \"continue\" without integer operand in \"switch\" is disallowed, use \"break\" instead." + }, + { + "line": 42, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $result." + }, + { + "line": 43, + "column": 9, + "type": "WARNING", + "source": "Squiz.PHP.NonExecutableCode.ReturnNotRequired", + "message": "Empty return statement not required here" + }, + { + "line": 43, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing.IncorrectLinesCountBeforeControlStructure", + "message": "Expected 1 line before \"return\", found 0." + } + ] + }, + "functions.php": { + "errors": 14, + "warnings": 1, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 7, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"FunctionsFixture.php\"" + }, + { + "line": 13, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $fn." + }, + { + "line": 13, + "column": 15, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.RequireArrowFunction.RequiredArrowFunction", + "message": "Use arrow function." + }, + { + "line": 19, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $closure." + }, + { + "line": 19, + "column": 20, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.RequireArrowFunction.RequiredArrowFunction", + "message": "Use arrow function." + }, + { + "line": 19, + "column": 37, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.UnusedInheritedVariablePassedToClosure.UnusedInheritedVariable", + "message": "Unused inherited variable $unused passed to closure." + }, + { + "line": 28, + "column": 26, + "type": "ERROR", + "source": "SlevomatCodingStandard.PHP.DisallowReference.DisallowedAssigningByReference", + "message": "Assigning by reference is disallowed." + }, + { + "line": 34, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 34, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + }, + { + "line": 39, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 39, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + }, + { + "line": 46, + "column": 1, + "type": "WARNING", + "source": "Squiz.Functions.GlobalFunction.Found", + "message": "Consider putting global function \"globalFunction\" in a static class" + }, + { + "line": 46, + "column": 1, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 46, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + } + ] + }, + "namespaces.php": { + "errors": 13, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 8, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse", + "message": "Type DateTime is not used in this file." + }, + { + "line": 9, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse", + "message": "Type ArrayObject is not used in this file." + }, + { + "line": 12, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse", + "message": "Type Exception is not used in this file." + }, + { + "line": 12, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UseSpacing.IncorrectLinesCountBetweenSameTypeOfUse", + "message": "Expected 0 lines between same types of use statement, found 1." + }, + { + "line": 15, + "column": 1, + "type": "ERROR", + "source": "PSR2.Namespaces.UseDeclaration.MultipleDeclarations", + "message": "There must be one USE keyword per declaration" + }, + { + "line": 15, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.DisallowGroupUse.DisallowedGroupUse", + "message": "Group use declaration is disallowed, use single use for every import." + }, + { + "line": 15, + "column": 13, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.MultipleUsesPerLine.MultipleUsesPerLine", + "message": "Multiple used types per use statement are forbidden." + }, + { + "line": 18, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse", + "message": "Type stdClass is not used in this file." + }, + { + "line": 18, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UseSpacing.IncorrectLinesCountBetweenSameTypeOfUse", + "message": "Expected 0 lines between same types of use statement, found 4." + }, + { + "line": 18, + "column": 5, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UseDoesNotStartWithBackslash.UseStartsWithBackslash", + "message": "Use statement cannot start with a backslash." + }, + { + "line": 20, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"NamespacesFixture.php\"" + }, + { + "line": 26, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $date." + } + ] + }, + "operators.php": { + "errors": 12, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 7, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"OperatorsFixture.php\"" + }, + { + "line": 16, + "column": 16, + "type": "ERROR", + "source": "SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator", + "message": "Operator == is disallowed, use === instead." + }, + { + "line": 21, + "column": 16, + "type": "ERROR", + "source": "SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator", + "message": "Operator != is disallowed, use !== instead." + }, + { + "line": 26, + "column": 12, + "type": "ERROR", + "source": "Squiz.Operators.IncrementDecrementUsage.Found", + "message": "Increment operators should be used where possible; found \"$a = $a + 1;\" but expected \"++$a\"" + }, + { + "line": 26, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Operators.RequireCombinedAssignmentOperator.RequiredCombinedAssignmentOperator", + "message": "Use \"+=\" operator instead of \"=\" and \"+\"." + }, + { + "line": 29, + "column": 20, + "type": "ERROR", + "source": "Generic.PHP.LowerCaseKeyword.Found", + "message": "PHP keywords must be lowercase; expected \"and\" but found \"AND\"" + }, + { + "line": 29, + "column": 20, + "type": "ERROR", + "source": "Squiz.Operators.ValidLogicalOperators.NotAllowed", + "message": "Logical operator \"and\" is prohibited; use \"&&\" instead" + }, + { + "line": 33, + "column": 20, + "type": "ERROR", + "source": "Generic.PHP.LowerCaseKeyword.Found", + "message": "PHP keywords must be lowercase; expected \"or\" but found \"OR\"" + }, + { + "line": 33, + "column": 20, + "type": "ERROR", + "source": "Squiz.Operators.ValidLogicalOperators.NotAllowed", + "message": "Logical operator \"or\" is prohibited; use \"||\" instead" + }, + { + "line": 38, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $bigNumber." + }, + { + "line": 38, + "column": 22, + "type": "ERROR", + "source": "SlevomatCodingStandard.Numbers.DisallowNumericLiteralSeparator.DisallowedNumericLiteralSeparator", + "message": "Use of numeric literal separator is disallowed." + } + ] + }, + "types.php": { + "errors": 13, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 7, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"TypesFixture.php\"" + }, + { + "line": 11, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingAnyTypeHint", + "message": "Property \\Tests\\Fixtures\\TypesFixture::$untyped does not have native type hint nor @var annotation for its value." + }, + { + "line": 11, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.VariableComment.WrongStyle", + "message": "You must use \"/**\" style comments for a member variable comment" + }, + { + "line": 14, + "column": 20, + "type": "ERROR", + "source": "Squiz.Commenting.VariableComment.WrongStyle", + "message": "You must use \"/**\" style comments for a member variable comment" + }, + { + "line": 17, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 17, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint", + "message": "Method \\Tests\\Fixtures\\TypesFixture::noParamType() does not have parameter type hint nor @param annotation for its parameter $param." + }, + { + "line": 17, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint", + "message": "Method \\Tests\\Fixtures\\TypesFixture::noParamType() does not have return type hint nor @return annotation for its return value." + }, + { + "line": 23, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 23, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint", + "message": "Method \\Tests\\Fixtures\\TypesFixture::noReturnType() does not have return type hint nor @return annotation for its return value." + }, + { + "line": 29, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 29, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + }, + { + "line": 29, + "column": 40, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue.NullabilityTypeMissing", + "message": "Parameter $param has null default value, but is not marked as nullable." + } + ] + } + } +} diff --git a/tests/Sets/next/Fixtures/Clean.php b/tests/Sets/next/Fixtures/Clean.php new file mode 100644 index 0000000..11158b2 --- /dev/null +++ b/tests/Sets/next/Fixtures/Clean.php @@ -0,0 +1,17 @@ +property ?? 'default'; + } + +} diff --git a/tests/Sets/next/Fixtures/DummyClass.php b/tests/Sets/next/Fixtures/DummyClass.php new file mode 100644 index 0000000..511f1d4 --- /dev/null +++ b/tests/Sets/next/Fixtures/DummyClass.php @@ -0,0 +1,8 @@ +uselessDefault('value'); + + // Reference usage - violation + $arr = [1, 2, 3]; + foreach ($arr as &$item) { + $item *= 2; + } + } + + // Empty function - violation + public function emptyFunction(): void + { + } + + // Useless default value (required param after optional) - violation + public function uselessDefault(string $required, string $optional = 'default'): void + { + } + +} + +// Global function - violation +function globalFunction(): void +{ +} diff --git a/tests/Sets/next/Fixtures/namespaces.php b/tests/Sets/next/Fixtures/namespaces.php new file mode 100644 index 0000000..c10cd2b --- /dev/null +++ b/tests/Sets/next/Fixtures/namespaces.php @@ -0,0 +1,29 @@ + 0 AND $b > 0) { + echo 'both positive'; + } + + if ($a > 0 OR $b > 0) { + echo 'one positive'; + } + + // Numeric literal separator - violation + $bigNumber = 1_000_000; + } + +} diff --git a/tests/Sets/next/Fixtures/types.php b/tests/Sets/next/Fixtures/types.php new file mode 100644 index 0000000..c9b0e0e --- /dev/null +++ b/tests/Sets/next/Fixtures/types.php @@ -0,0 +1,33 @@ + + + + diff --git a/tests/Sets/next/snapshot.json b/tests/Sets/next/snapshot.json new file mode 100644 index 0000000..dae5eaa --- /dev/null +++ b/tests/Sets/next/snapshot.json @@ -0,0 +1,759 @@ +{ + "totals": { + "errors": 97, + "warnings": 2 + }, + "files": { + "Clean.php": { + "errors": 0, + "warnings": 0, + "messages": [] + }, + "DummyClass.php": { + "errors": 0, + "warnings": 0, + "messages": [] + }, + "arrays.php": { + "errors": 9, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 8, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $arr." + }, + { + "line": 8, + "column": 8, + "type": "ERROR", + "source": "Generic.Arrays.DisallowLongArraySyntax.Found", + "message": "Short array syntax must be used to define arrays" + }, + { + "line": 11, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $multiline." + }, + { + "line": 13, + "column": 5, + "type": "ERROR", + "source": "SlevomatCodingStandard.Arrays.TrailingArrayComma.MissingTrailingComma", + "message": "Multi-line arrays must have a trailing comma after the last element." + }, + { + "line": 17, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Arrays.DisallowImplicitArrayCreation.ImplicitArrayCreationUsed", + "message": "Implicit array creation is disallowed." + }, + { + "line": 20, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $spaced." + }, + { + "line": 20, + "column": 11, + "type": "ERROR", + "source": "SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace.SpaceAfterArrayOpen", + "message": "Expected 0 spaces after array opening bracket, 1 found." + }, + { + "line": 20, + "column": 21, + "type": "ERROR", + "source": "SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace.SpaceBeforeArrayClose", + "message": "Expected 0 spaces before array closing bracket, 1 found." + } + ] + }, + "classes.php": { + "errors": 15, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 8, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"ClassesFixture.php\"" + }, + { + "line": 9, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces.NoEmptyLineAfterOpeningBrace", + "message": "There must be one empty line after class opening brace." + }, + { + "line": 11, + "column": 5, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.ClassConstantVisibility.MissingConstantVisibility", + "message": "Constant \\Tests\\Fixtures\\ClassesFixture::FOO visibility missing." + }, + { + "line": 14, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition.DisallowedMultiConstantDefinition", + "message": "Use of multi constant definition is disallowed." + }, + { + "line": 17, + "column": 19, + "type": "ERROR", + "source": "Squiz.Commenting.VariableComment.WrongStyle", + "message": "You must use \"/**\" style comments for a member variable comment" + }, + { + "line": 20, + "column": 5, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition.DisallowedMultiPropertyDefinition", + "message": "Use of multi property definition is disallowed." + }, + { + "line": 20, + "column": 16, + "type": "ERROR", + "source": "PSR2.Classes.PropertyDeclaration.Multiple", + "message": "There must not be more than one property declared per statement" + }, + { + "line": 20, + "column": 16, + "type": "ERROR", + "source": "Squiz.Commenting.VariableComment.WrongStyle", + "message": "You must use \"/**\" style comments for a member variable comment" + }, + { + "line": 23, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 29, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces.NoEmptyLineBeforeClosingBrace", + "message": "There must be one empty line before class closing brace." + }, + { + "line": 32, + "column": 1, + "type": "ERROR", + "source": "PSR1.Classes.ClassDeclaration.MultipleClasses", + "message": "Each class must be in a file by itself" + }, + { + "line": 32, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match interface name; expected file name \"IWrongNaming.php\"" + }, + { + "line": 38, + "column": 1, + "type": "ERROR", + "source": "PSR1.Classes.ClassDeclaration.MultipleClasses", + "message": "Each interface must be in a file by itself" + }, + { + "line": 38, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match trait name; expected file name \"TWrongNaming.php\"" + } + ] + }, + "comments.php": { + "errors": 10, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 8, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @author is forbidden." + }, + { + "line": 9, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @copyright is forbidden." + }, + { + "line": 10, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @package is forbidden." + }, + { + "line": 11, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @since is forbidden." + }, + { + "line": 12, + "column": 4, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden", + "message": "Use of annotation @todo is forbidden." + }, + { + "line": 14, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"CommentsFixture.php\"" + }, + { + "line": 24, + "column": 8, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenComments.CommentForbidden", + "message": "Documentation comment contains forbidden comment \"Constructor.\"." + }, + { + "line": 27, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + }, + { + "line": 32, + "column": 8, + "type": "ERROR", + "source": "SlevomatCodingStandard.Commenting.ForbiddenComments.CommentForbidden", + "message": "Documentation comment contains forbidden comment \"Name getter.\"." + } + ] + }, + "control-structures.php": { + "errors": 11, + "warnings": 1, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 7, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"ControlStructuresFixture.php\"" + }, + { + "line": 16, + "column": 15, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.DisallowYodaComparison.DisallowedYodaComparison", + "message": "Yoda comparisons are disallowed." + }, + { + "line": 23, + "column": 11, + "type": "ERROR", + "source": "PSR2.ControlStructures.ElseIfDeclaration.NotAllowed", + "message": "Usage of ELSE IF is discouraged; use ELSEIF instead" + }, + { + "line": 28, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $c." + }, + { + "line": 28, + "column": 26, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator.NullCoalesceOperatorNotUsed", + "message": "Use null coalesce operator instead of ternary operator." + }, + { + "line": 31, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $d." + }, + { + "line": 31, + "column": 23, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.UselessTernaryOperator.UselessTernaryOperator", + "message": "Useless ternary operator." + }, + { + "line": 36, + "column": 17, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch.DisallowedContinueWithoutIntegerOperandInSwitch", + "message": "Usage of \"continue\" without integer operand in \"switch\" is disallowed, use \"break\" instead." + }, + { + "line": 42, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $result." + }, + { + "line": 43, + "column": 9, + "type": "WARNING", + "source": "Squiz.PHP.NonExecutableCode.ReturnNotRequired", + "message": "Empty return statement not required here" + }, + { + "line": 43, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing.IncorrectLinesCountBeforeControlStructure", + "message": "Expected 1 line before \"return\", found 0." + } + ] + }, + "functions.php": { + "errors": 14, + "warnings": 1, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 7, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"FunctionsFixture.php\"" + }, + { + "line": 13, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $fn." + }, + { + "line": 13, + "column": 15, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.RequireArrowFunction.RequiredArrowFunction", + "message": "Use arrow function." + }, + { + "line": 19, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $closure." + }, + { + "line": 19, + "column": 20, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.RequireArrowFunction.RequiredArrowFunction", + "message": "Use arrow function." + }, + { + "line": 19, + "column": 37, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.UnusedInheritedVariablePassedToClosure.UnusedInheritedVariable", + "message": "Unused inherited variable $unused passed to closure." + }, + { + "line": 28, + "column": 26, + "type": "ERROR", + "source": "SlevomatCodingStandard.PHP.DisallowReference.DisallowedAssigningByReference", + "message": "Assigning by reference is disallowed." + }, + { + "line": 34, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 34, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + }, + { + "line": 39, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 39, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + }, + { + "line": 46, + "column": 1, + "type": "WARNING", + "source": "Squiz.Functions.GlobalFunction.Found", + "message": "Consider putting global function \"globalFunction\" in a static class" + }, + { + "line": 46, + "column": 1, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 46, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + } + ] + }, + "namespaces.php": { + "errors": 13, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 8, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse", + "message": "Type DateTime is not used in this file." + }, + { + "line": 9, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse", + "message": "Type ArrayObject is not used in this file." + }, + { + "line": 12, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse", + "message": "Type Exception is not used in this file." + }, + { + "line": 12, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UseSpacing.IncorrectLinesCountBetweenSameTypeOfUse", + "message": "Expected 0 lines between same types of use statement, found 1." + }, + { + "line": 15, + "column": 1, + "type": "ERROR", + "source": "PSR2.Namespaces.UseDeclaration.MultipleDeclarations", + "message": "There must be one USE keyword per declaration" + }, + { + "line": 15, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.DisallowGroupUse.DisallowedGroupUse", + "message": "Group use declaration is disallowed, use single use for every import." + }, + { + "line": 15, + "column": 13, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.MultipleUsesPerLine.MultipleUsesPerLine", + "message": "Multiple used types per use statement are forbidden." + }, + { + "line": 18, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse", + "message": "Type stdClass is not used in this file." + }, + { + "line": 18, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UseSpacing.IncorrectLinesCountBetweenSameTypeOfUse", + "message": "Expected 0 lines between same types of use statement, found 4." + }, + { + "line": 18, + "column": 5, + "type": "ERROR", + "source": "SlevomatCodingStandard.Namespaces.UseDoesNotStartWithBackslash.UseStartsWithBackslash", + "message": "Use statement cannot start with a backslash." + }, + { + "line": 20, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"NamespacesFixture.php\"" + }, + { + "line": 26, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $date." + } + ] + }, + "operators.php": { + "errors": 12, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 7, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"OperatorsFixture.php\"" + }, + { + "line": 16, + "column": 16, + "type": "ERROR", + "source": "SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator", + "message": "Operator == is disallowed, use === instead." + }, + { + "line": 21, + "column": 16, + "type": "ERROR", + "source": "SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator", + "message": "Operator != is disallowed, use !== instead." + }, + { + "line": 26, + "column": 12, + "type": "ERROR", + "source": "Squiz.Operators.IncrementDecrementUsage.Found", + "message": "Increment operators should be used where possible; found \"$a = $a + 1;\" but expected \"++$a\"" + }, + { + "line": 26, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Operators.RequireCombinedAssignmentOperator.RequiredCombinedAssignmentOperator", + "message": "Use \"+=\" operator instead of \"=\" and \"+\"." + }, + { + "line": 29, + "column": 20, + "type": "ERROR", + "source": "Generic.PHP.LowerCaseKeyword.Found", + "message": "PHP keywords must be lowercase; expected \"and\" but found \"AND\"" + }, + { + "line": 29, + "column": 20, + "type": "ERROR", + "source": "Squiz.Operators.ValidLogicalOperators.NotAllowed", + "message": "Logical operator \"and\" is prohibited; use \"&&\" instead" + }, + { + "line": 33, + "column": 20, + "type": "ERROR", + "source": "Generic.PHP.LowerCaseKeyword.Found", + "message": "PHP keywords must be lowercase; expected \"or\" but found \"OR\"" + }, + { + "line": 33, + "column": 20, + "type": "ERROR", + "source": "Squiz.Operators.ValidLogicalOperators.NotAllowed", + "message": "Logical operator \"or\" is prohibited; use \"||\" instead" + }, + { + "line": 38, + "column": 9, + "type": "ERROR", + "source": "SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable", + "message": "Unused variable $bigNumber." + }, + { + "line": 38, + "column": 22, + "type": "ERROR", + "source": "SlevomatCodingStandard.Numbers.DisallowNumericLiteralSeparator.DisallowedNumericLiteralSeparator", + "message": "Use of numeric literal separator is disallowed." + } + ] + }, + "types.php": { + "errors": 13, + "warnings": 0, + "messages": [ + { + "line": 1, + "column": 1, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing", + "message": "Missing declare(strict_types = 1)." + }, + { + "line": 7, + "column": 1, + "type": "ERROR", + "source": "Squiz.Classes.ClassFileName.NoMatch", + "message": "Filename doesn't match class name; expected file name \"TypesFixture.php\"" + }, + { + "line": 11, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingAnyTypeHint", + "message": "Property \\Tests\\Fixtures\\TypesFixture::$untyped does not have native type hint nor @var annotation for its value." + }, + { + "line": 11, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.VariableComment.WrongStyle", + "message": "You must use \"/**\" style comments for a member variable comment" + }, + { + "line": 14, + "column": 20, + "type": "ERROR", + "source": "Squiz.Commenting.VariableComment.WrongStyle", + "message": "You must use \"/**\" style comments for a member variable comment" + }, + { + "line": 17, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 17, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint", + "message": "Method \\Tests\\Fixtures\\TypesFixture::noParamType() does not have parameter type hint nor @param annotation for its parameter $param." + }, + { + "line": 17, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint", + "message": "Method \\Tests\\Fixtures\\TypesFixture::noParamType() does not have return type hint nor @return annotation for its return value." + }, + { + "line": 23, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 23, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingAnyTypeHint", + "message": "Method \\Tests\\Fixtures\\TypesFixture::noReturnType() does not have return type hint nor @return annotation for its return value." + }, + { + "line": 29, + "column": 12, + "type": "ERROR", + "source": "Squiz.Commenting.FunctionComment.WrongStyle", + "message": "You must use \"/**\" style comments for a function comment" + }, + { + "line": 29, + "column": 12, + "type": "ERROR", + "source": "SlevomatCodingStandard.Functions.DisallowEmptyFunction.EmptyFunction", + "message": "Empty function body must have at least a comment to explain why is empty." + }, + { + "line": 29, + "column": 40, + "type": "ERROR", + "source": "SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue.NullabilityTypeMissing", + "message": "Parameter $param has null default value, but is not marked as nullable." + } + ] + } + } +} From ac7cf75a12e155696de5823ed78072a88cafc1ee Mon Sep 17 00:00:00 2001 From: OhMyFelix Date: Mon, 23 Feb 2026 08:02:05 +0000 Subject: [PATCH 2/7] CI/docs: add snapshot drift guard and 8.5 docs --- .docs/README.md | 6 +- .docs/rulesets/ruleset-8.5.md | 206 ++++++++++++++++++++++++++++++++++ .github/workflows/tests.yaml | 22 ++++ Makefile | 1 + tests/Toolkit/Codesniffer.php | 11 +- 5 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 .docs/rulesets/ruleset-8.5.md diff --git a/.docs/README.md b/.docs/README.md index de0c172..d44e3fc 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -31,8 +31,8 @@ Take a look at our template repository [contributte/bare](https://github.com/con - - + + @@ -66,7 +66,7 @@ vendor/bin/phpcs --standard=ruleset.xml -e Example output: ``` -The Contributte standard contains 186 sniffs +The Contributte standard contains 185 sniffs Generic (27 sniffs) ------------------- diff --git a/.docs/rulesets/ruleset-8.5.md b/.docs/rulesets/ruleset-8.5.md new file mode 100644 index 0000000..fcda346 --- /dev/null +++ b/.docs/rulesets/ruleset-8.5.md @@ -0,0 +1,206 @@ +# Ruleset 8.5\n + +The Contributte standard contains 185 sniffs + +Generic (28 sniffs) +------------------- + Generic.Arrays.DisallowLongArraySyntax + Generic.Classes.DuplicateClassName + Generic.CodeAnalysis.EmptyStatement + Generic.CodeAnalysis.ForLoopShouldBeWhileLoop + Generic.CodeAnalysis.UnconditionalIfStatement + Generic.CodeAnalysis.UnnecessaryFinalModifier + Generic.Commenting.DocComment + Generic.Files.ByteOrderMark + Generic.Files.InlineHTML + Generic.Files.LineEndings + Generic.Formatting.DisallowMultipleStatements + Generic.Formatting.SpaceAfterCast + Generic.Functions.FunctionCallArgumentSpacing + Generic.Functions.OpeningFunctionBraceBsdAllman + Generic.NamingConventions.CamelCapsFunctionName + Generic.NamingConventions.ConstructorName + Generic.NamingConventions.UpperCaseConstantName + Generic.PHP.CharacterBeforePHPOpeningTag + Generic.PHP.DeprecatedFunctions + Generic.PHP.DisallowAlternativePHPTags + Generic.PHP.DisallowShortOpenTag + Generic.PHP.ForbiddenFunctions + Generic.PHP.LowerCaseConstant + Generic.PHP.LowerCaseKeyword + Generic.Strings.UnnecessaryStringConcat + Generic.WhiteSpace.DisallowSpaceIndent + Generic.WhiteSpace.LanguageConstructSpacing + Generic.WhiteSpace.ScopeIndent + +PEAR (4 sniffs) +--------------- + PEAR.Classes.ClassDeclaration + PEAR.Commenting.InlineComment + PEAR.Formatting.MultiLineAssignment + PEAR.WhiteSpace.ObjectOperatorIndent + +PSR1 (3 sniffs) +--------------- + PSR1.Classes.ClassDeclaration + PSR1.Files.SideEffects + PSR1.Methods.CamelCapsMethodName + +PSR2 (11 sniffs) +---------------- + PSR2.Classes.PropertyDeclaration + PSR2.ControlStructures.ControlStructureSpacing + PSR2.ControlStructures.ElseIfDeclaration + PSR2.ControlStructures.SwitchDeclaration + PSR2.Files.ClosingTag + PSR2.Files.EndFileNewline + PSR2.Methods.FunctionCallSignature + PSR2.Methods.FunctionClosingBrace + PSR2.Methods.MethodDeclaration + PSR2.Namespaces.NamespaceDeclaration + PSR2.Namespaces.UseDeclaration + +SlevomatCodingStandard (100 sniffs) +----------------------------------- + SlevomatCodingStandard.Arrays.DisallowImplicitArrayCreation + SlevomatCodingStandard.Arrays.MultiLineArrayEndBracketPlacement + SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace + SlevomatCodingStandard.Arrays.TrailingArrayComma + SlevomatCodingStandard.Classes.ClassConstantVisibility + SlevomatCodingStandard.Classes.ClassMemberSpacing + SlevomatCodingStandard.Classes.ClassStructure + SlevomatCodingStandard.Classes.ConstantSpacing + SlevomatCodingStandard.Classes.DisallowLateStaticBindingForConstants + SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition + SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition + SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces + SlevomatCodingStandard.Classes.MethodSpacing + SlevomatCodingStandard.Classes.ModernClassNameReference + SlevomatCodingStandard.Classes.ParentCallSpacing + SlevomatCodingStandard.Classes.PropertySpacing + SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming + SlevomatCodingStandard.Classes.SuperfluousErrorNaming + SlevomatCodingStandard.Classes.SuperfluousExceptionNaming + SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming + SlevomatCodingStandard.Classes.SuperfluousTraitNaming + SlevomatCodingStandard.Classes.TraitUseDeclaration + SlevomatCodingStandard.Classes.TraitUseSpacing + SlevomatCodingStandard.Classes.UselessLateStaticBinding + SlevomatCodingStandard.Commenting.DeprecatedAnnotationDeclaration + SlevomatCodingStandard.Commenting.DisallowOneLinePropertyDocComment + SlevomatCodingStandard.Commenting.DocCommentSpacing + SlevomatCodingStandard.Commenting.EmptyComment + SlevomatCodingStandard.Commenting.ForbiddenAnnotations + SlevomatCodingStandard.Commenting.ForbiddenComments + SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration + SlevomatCodingStandard.Commenting.RequireOneLinePropertyDocComment + SlevomatCodingStandard.Commenting.UselessFunctionDocComment + SlevomatCodingStandard.Commenting.UselessInheritDocComment + SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing + SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch + SlevomatCodingStandard.ControlStructures.DisallowYodaComparison + SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing + SlevomatCodingStandard.ControlStructures.LanguageConstructWithParentheses + SlevomatCodingStandard.ControlStructures.NewWithoutParentheses + SlevomatCodingStandard.ControlStructures.NewWithParentheses + SlevomatCodingStandard.ControlStructures.RequireMultiLineTernaryOperator + SlevomatCodingStandard.ControlStructures.RequireNullCoalesceEqualOperator + SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator + SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator + SlevomatCodingStandard.ControlStructures.RequireSingleLineCondition + SlevomatCodingStandard.ControlStructures.RequireTernaryOperator + SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn + SlevomatCodingStandard.ControlStructures.UselessTernaryOperator + SlevomatCodingStandard.Exceptions.DeadCatch + SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly + SlevomatCodingStandard.Functions.ArrowFunctionDeclaration + SlevomatCodingStandard.Functions.DisallowEmptyFunction + SlevomatCodingStandard.Functions.RequireArrowFunction + SlevomatCodingStandard.Functions.StaticClosure + SlevomatCodingStandard.Functions.StrictCall + SlevomatCodingStandard.Functions.UnusedInheritedVariablePassedToClosure + SlevomatCodingStandard.Functions.UselessParameterDefaultValue + SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses + SlevomatCodingStandard.Namespaces.DisallowGroupUse + SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation + SlevomatCodingStandard.Namespaces.FullyQualifiedExceptions + SlevomatCodingStandard.Namespaces.MultipleUsesPerLine + SlevomatCodingStandard.Namespaces.NamespaceDeclaration + SlevomatCodingStandard.Namespaces.NamespaceSpacing + SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + SlevomatCodingStandard.Namespaces.RequireOneNamespaceInFile + SlevomatCodingStandard.Namespaces.UnusedUses + SlevomatCodingStandard.Namespaces.UseDoesNotStartWithBackslash + SlevomatCodingStandard.Namespaces.UseFromSameNamespace + SlevomatCodingStandard.Namespaces.UselessAlias + SlevomatCodingStandard.Namespaces.UseSpacing + SlevomatCodingStandard.Numbers.DisallowNumericLiteralSeparator + SlevomatCodingStandard.Operators.DisallowEqualOperators + SlevomatCodingStandard.Operators.NegationOperatorSpacing + SlevomatCodingStandard.Operators.RequireCombinedAssignmentOperator + SlevomatCodingStandard.Operators.SpreadOperatorSpacing + SlevomatCodingStandard.PHP.DisallowDirectMagicInvokeCall + SlevomatCodingStandard.PHP.DisallowReference + SlevomatCodingStandard.PHP.OptimizedFunctionsWithoutUnpacking + SlevomatCodingStandard.PHP.ReferenceSpacing + SlevomatCodingStandard.PHP.RequireNowdoc + SlevomatCodingStandard.PHP.ShortList + SlevomatCodingStandard.PHP.TypeCast + SlevomatCodingStandard.PHP.UselessSemicolon + SlevomatCodingStandard.TypeHints.DeclareStrictTypes + SlevomatCodingStandard.TypeHints.LongTypeHints + SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue + SlevomatCodingStandard.TypeHints.NullTypeHintOnLastPosition + SlevomatCodingStandard.TypeHints.ParameterTypeHint + SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing + SlevomatCodingStandard.TypeHints.PropertyTypeHint + SlevomatCodingStandard.TypeHints.ReturnTypeHint + SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing + SlevomatCodingStandard.TypeHints.UselessConstantTypeHint + SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable + SlevomatCodingStandard.Variables.DuplicateAssignmentToVariable + SlevomatCodingStandard.Variables.UnusedVariable + SlevomatCodingStandard.Variables.UselessVariable + SlevomatCodingStandard.Whitespaces.DuplicateSpaces + +Squiz (39 sniffs) +----------------- + Squiz.Arrays.ArrayBracketSpacing + Squiz.Arrays.ArrayDeclaration + Squiz.Classes.ClassFileName + Squiz.Classes.SelfMemberReference + Squiz.Classes.ValidClassName + Squiz.Commenting.DocCommentAlignment + Squiz.Commenting.EmptyCatchComment + Squiz.Commenting.FunctionComment + Squiz.Commenting.VariableComment + Squiz.ControlStructures.ControlSignature + Squiz.ControlStructures.ForEachLoopDeclaration + Squiz.ControlStructures.ForLoopDeclaration + Squiz.Functions.FunctionDeclaration + Squiz.Functions.FunctionDeclarationArgumentSpacing + Squiz.Functions.GlobalFunction + Squiz.Functions.MultiLineFunctionDeclaration + Squiz.Operators.IncrementDecrementUsage + Squiz.Operators.ValidLogicalOperators + Squiz.PHP.GlobalKeyword + Squiz.PHP.Heredoc + Squiz.PHP.InnerFunctions + Squiz.PHP.LowercasePHPFunctions + Squiz.PHP.NonExecutableCode + Squiz.Scope.MethodScope + Squiz.Scope.StaticThisUsage + Squiz.Strings.ConcatenationSpacing + Squiz.Strings.DoubleQuoteUsage + Squiz.Strings.EchoedStrings + Squiz.WhiteSpace.CastSpacing + Squiz.WhiteSpace.ControlStructureSpacing + Squiz.WhiteSpace.FunctionOpeningBraceSpace + Squiz.WhiteSpace.FunctionSpacing + Squiz.WhiteSpace.LogicalOperatorSpacing + Squiz.WhiteSpace.ObjectOperatorSpacing + Squiz.WhiteSpace.OperatorSpacing + Squiz.WhiteSpace.ScopeClosingBrace + Squiz.WhiteSpace.ScopeKeywordSpacing + Squiz.WhiteSpace.SemicolonSpacing + Squiz.WhiteSpace.SuperfluousWhitespace diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 42df98d..3f8de5f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -40,3 +40,25 @@ jobs: with: php: "8.2" composer: "composer update --no-interaction --no-progress --prefer-dist --prefer-stable --prefer-lowest" + + snapshots: + name: "Snapshots" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: "8.5" + coverage: none + + - name: "Install dependencies" + run: composer install --no-interaction --no-progress --prefer-dist + + - name: "Regenerate snapshots" + run: php bin/snapshots + + - name: "Check for snapshot drift" + run: git diff --exit-code diff --git a/Makefile b/Makefile index 2ea164b..f6d6d08 100755 --- a/Makefile +++ b/Makefile @@ -19,4 +19,5 @@ docs: echo "# Ruleset 8.2\n" > .docs/rulesets/ruleset-8.2.md && vendor/bin/phpcs --standard=ruleset-8.2.xml -e >> .docs/rulesets/ruleset-8.2.md echo "# Ruleset 8.3\n" > .docs/rulesets/ruleset-8.3.md && vendor/bin/phpcs --standard=ruleset-8.3.xml -e >> .docs/rulesets/ruleset-8.3.md echo "# Ruleset 8.4\n" > .docs/rulesets/ruleset-8.4.md && vendor/bin/phpcs --standard=ruleset-8.4.xml -e >> .docs/rulesets/ruleset-8.4.md + echo "# Ruleset 8.5\n" > .docs/rulesets/ruleset-8.5.md && vendor/bin/phpcs --standard=ruleset-8.5.xml -e >> .docs/rulesets/ruleset-8.5.md echo "# Ruleset next\n" > .docs/rulesets/ruleset-next.md && vendor/bin/phpcs --standard=ruleset-next.xml -e >> .docs/rulesets/ruleset-next.md diff --git a/tests/Toolkit/Codesniffer.php b/tests/Toolkit/Codesniffer.php index e235e8a..85cf8b1 100644 --- a/tests/Toolkit/Codesniffer.php +++ b/tests/Toolkit/Codesniffer.php @@ -28,10 +28,19 @@ public static function normalize(array $output): array $filePaths = array_keys($output['files']); sort($filePaths); + $fileNameCounts = array_count_values(array_map('basename', $filePaths)); + $fileNameIndexes = []; foreach ($filePaths as $filePath) { $fileData = $output['files'][$filePath]; $fileName = basename($filePath); + $fileKey = $fileName; + + if (($fileNameCounts[$fileName] ?? 0) > 1) { + $fileNameIndexes[$fileName] = ($fileNameIndexes[$fileName] ?? 0) + 1; + $fileKey = sprintf('%s#%d', $fileName, $fileNameIndexes[$fileName]); + } + $messages = []; foreach ($fileData['messages'] ?? [] as $message) { @@ -50,7 +59,7 @@ public static function normalize(array $output): array : $a['column'] <=> $b['column']; }); - $normalized['files'][$fileName] = [ + $normalized['files'][$fileKey] = [ 'errors' => $fileData['errors'] ?? 0, 'warnings' => $fileData['warnings'] ?? 0, 'messages' => $messages, From a9854372c151337706e2a285a83778e1b9f2c366 Mon Sep 17 00:00:00 2001 From: OhMyFelix Date: Mon, 23 Feb 2026 08:45:34 +0000 Subject: [PATCH 3/7] CI: clarify workflow and job naming --- .github/workflows/tests.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3f8de5f..c87400a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,4 +1,4 @@ -name: "Nette Tester" +name: "Tests" on: pull_request: @@ -11,31 +11,31 @@ on: jobs: test85: - name: "Nette Tester" + name: "PHP 8.5" uses: contributte/.github/.github/workflows/nette-tester.yml@v1 with: php: "8.5" test84: - name: "Nette Tester" + name: "PHP 8.4" uses: contributte/.github/.github/workflows/nette-tester.yml@v1 with: php: "8.4" test83: - name: "Nette Tester" + name: "PHP 8.3" uses: contributte/.github/.github/workflows/nette-tester.yml@v1 with: php: "8.3" test82: - name: "Nette Tester" + name: "PHP 8.2" uses: contributte/.github/.github/workflows/nette-tester.yml@v1 with: php: "8.2" testlower: - name: "Nette Tester" + name: "Lowest dependencies" uses: contributte/.github/.github/workflows/nette-tester.yml@v1 with: php: "8.2" From f7c42a722be6c9c43cbda62819de8abc2bf28807 Mon Sep 17 00:00:00 2001 From: OhMyFelix Date: Mon, 23 Feb 2026 08:52:13 +0000 Subject: [PATCH 4/7] CI: add ruleset parity and diff reporting --- .dev/check-ruleset-sets-parity.sh | 44 +++++++++++++++++ .dev/ruleset-diff-report.sh | 78 +++++++++++++++++++++++++++++++ .github/workflows/tests.yaml | 12 +++++ 3 files changed, 134 insertions(+) create mode 100644 .dev/check-ruleset-sets-parity.sh create mode 100644 .dev/ruleset-diff-report.sh diff --git a/.dev/check-ruleset-sets-parity.sh b/.dev/check-ruleset-sets-parity.sh new file mode 100644 index 0000000..1ba81c3 --- /dev/null +++ b/.dev/check-ruleset-sets-parity.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" + +tmp_expected=$(mktemp) +tmp_actual=$(mktemp) +trap 'rm -f "$tmp_expected" "$tmp_actual"' EXIT + +for ruleset in "$ROOT_DIR"/ruleset-*.xml; do + name=$(basename "$ruleset") + name=${name#ruleset-} + name=${name%.xml} + echo "$name" +done | sort -u > "$tmp_expected" + +for set_dir in "$ROOT_DIR"/tests/Sets/*; do + if [ -d "$set_dir" ]; then + echo "$(basename "$set_dir")" + fi +done | sort -u > "$tmp_actual" + +missing_sets=$(comm -23 "$tmp_expected" "$tmp_actual" || true) +orphan_sets=$(comm -13 "$tmp_expected" "$tmp_actual" || true) + +if [ -n "$missing_sets" ] || [ -n "$orphan_sets" ]; then + echo "Ruleset/set parity check failed." + + if [ -n "$missing_sets" ]; then + echo "Missing tests/Sets directories for rulesets:" + echo "$missing_sets" + fi + + if [ -n "$orphan_sets" ]; then + echo "tests/Sets directories without matching ruleset-*.xml:" + echo "$orphan_sets" + fi + + exit 1 +fi + +echo "Ruleset/set parity check passed." diff --git a/.dev/ruleset-diff-report.sh b/.dev/ruleset-diff-report.sh new file mode 100644 index 0000000..9a43f07 --- /dev/null +++ b/.dev/ruleset-diff-report.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +REPORT_DIR="$ROOT_DIR/.reports" +REPORT_FILE="$REPORT_DIR/ruleset-diff.md" + +mkdir -p "$REPORT_DIR" + +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +rulesets=("ruleset.xml" "ruleset-8.2.xml" "ruleset-8.3.xml" "ruleset-8.4.xml" "ruleset-8.5.xml" "ruleset-next.xml") + +extract_sniffs() { + local standard="$1" + vendor/bin/phpcs --standard="$standard" -e \ + | awk '$1 ~ /^[A-Z][A-Za-z0-9]*\.[A-Za-z0-9]+\.[A-Za-z0-9]+$/ {print $1}' +} + +for ruleset in "${rulesets[@]}"; do + extract_sniffs "$ruleset" | sort -u > "$tmp_dir/$ruleset.txt" +done + +{ + echo "# Ruleset Diff Report" + echo + echo "Generated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" + echo + echo "## Sniff Counts" + echo + echo "| Ruleset | Sniff count |" + echo "|---|---:|" + for ruleset in "${rulesets[@]}"; do + count=$(wc -l < "$tmp_dir/$ruleset.txt" | tr -d ' ') + echo "| \`$ruleset\` | $count |" + done + echo + + echo "## Differences vs ruleset.xml" + echo + for ruleset in "${rulesets[@]}"; do + if [ "$ruleset" = "ruleset.xml" ]; then + continue + fi + + added=$(comm -13 "$tmp_dir/ruleset.xml.txt" "$tmp_dir/$ruleset.txt" || true) + removed=$(comm -23 "$tmp_dir/ruleset.xml.txt" "$tmp_dir/$ruleset.txt" || true) + + echo "### \`$ruleset\`" + echo + if [ -z "$added" ] && [ -z "$removed" ]; then + echo "No sniff-list differences compared to \`ruleset.xml\`." + echo + continue + fi + + if [ -n "$added" ]; then + echo "Added sniffs:" + echo '```' + echo "$added" + echo '```' + echo + fi + + if [ -n "$removed" ]; then + echo "Removed sniffs:" + echo '```' + echo "$removed" + echo '```' + echo + fi + done +} > "$REPORT_FILE" + +echo "Generated $REPORT_FILE" diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c87400a..6cb29a7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -57,6 +57,18 @@ jobs: - name: "Install dependencies" run: composer install --no-interaction --no-progress --prefer-dist + - name: "Check ruleset and set parity" + run: bash .dev/check-ruleset-sets-parity.sh + + - name: "Build ruleset diff report" + run: bash .dev/ruleset-diff-report.sh + + - name: "Upload ruleset diff report" + uses: actions/upload-artifact@v4 + with: + name: ruleset-diff-report + path: .reports/ruleset-diff.md + - name: "Regenerate snapshots" run: php bin/snapshots From b05ba26beeb535a397a849332a88d3d67437ced6 Mon Sep 17 00:00:00 2001 From: OhMyFelix Date: Mon, 23 Feb 2026 09:45:36 +0000 Subject: [PATCH 5/7] Tests: cover Codesniffer duplicate filename normalization --- tests/Cases/CodesnifferTest.php | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/Cases/CodesnifferTest.php diff --git a/tests/Cases/CodesnifferTest.php b/tests/Cases/CodesnifferTest.php new file mode 100644 index 0000000..01fc5a0 --- /dev/null +++ b/tests/Cases/CodesnifferTest.php @@ -0,0 +1,51 @@ + ['errors' => 0, 'warnings' => 0], + 'files' => [ + '/tmp/alpha.php' => [ + 'errors' => 0, + 'warnings' => 0, + 'messages' => [], + ], + ], + ]); + + self::assertArrayHasKey('alpha.php', $normalized['files']); + self::assertArrayNotHasKey('alpha.php#1', $normalized['files']); + } + + public function testNormalizeKeepsDuplicateBasenamesUnique(): void + { + $normalized = Codesniffer::normalize([ + 'totals' => ['errors' => 2, 'warnings' => 0], + 'files' => [ + '/tmp/a/duplicate.php' => [ + 'errors' => 1, + 'warnings' => 0, + 'messages' => [], + ], + '/tmp/b/duplicate.php' => [ + 'errors' => 1, + 'warnings' => 0, + 'messages' => [], + ], + ], + ]); + + self::assertSame(['duplicate.php#1', 'duplicate.php#2'], array_keys($normalized['files'])); + self::assertSame(1, $normalized['files']['duplicate.php#1']['errors']); + self::assertSame(1, $normalized['files']['duplicate.php#2']['errors']); + } + +} From 0f85c02f59e3ae44e412092882e2c321c4050d53 Mon Sep 17 00:00:00 2001 From: OhMyFelix Date: Mon, 23 Feb 2026 09:54:23 +0000 Subject: [PATCH 6/7] CI: publish per-sniff coverage matrix artifact --- .dev/sniff-coverage-matrix.py | 144 ++++++++++++++++++++++++++++++++++ .github/workflows/tests.yaml | 11 +++ 2 files changed, 155 insertions(+) create mode 100644 .dev/sniff-coverage-matrix.py diff --git a/.dev/sniff-coverage-matrix.py b/.dev/sniff-coverage-matrix.py new file mode 100644 index 0000000..bf4aae3 --- /dev/null +++ b/.dev/sniff-coverage-matrix.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import csv +import re +import subprocess +from collections import defaultdict +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +REPORTS = ROOT / ".reports" +SNIFFS_ROOT = ROOT / "tests" / "Sniffs" + +SNIFF_RE = re.compile(r"^\s*([A-Z][A-Za-z0-9]*\.[A-Za-z0-9_]+\.[A-Za-z0-9_]+)\s*$") +RULE_REF_RE = re.compile(r" list[str]: + result = subprocess.run( + ["vendor/bin/phpcs", "--standard=ruleset.xml", "-e"], + cwd=ROOT, + text=True, + capture_output=True, + check=True, + ) + + sniffs: list[str] = [] + for line in result.stdout.splitlines(): + match = SNIFF_RE.match(line) + if match: + sniffs.append(match.group(1)) + + return sorted(set(sniffs)) + + +def build_ref_directory_map() -> dict[str, set[Path]]: + refs: dict[str, set[Path]] = defaultdict(set) + + for ruleset in SNIFFS_ROOT.glob("**/*.ruleset.xml"): + content = ruleset.read_text(encoding="utf-8") + for ref in RULE_REF_RE.findall(content): + if "." in ref: + refs[ref].add(ruleset.parent) + + return refs + + +def summarize_sniff(ref: str, directories: set[Path]) -> dict[str, object]: + fixture_paths: set[Path] = set() + has_good = False + has_bad = False + custom_count = 0 + + for directory in sorted(directories): + for php_file in directory.glob("*.php"): + fixture_paths.add(php_file) + if php_file.name == "good.php": + has_good = True + elif php_file.name == "bad.php": + has_bad = True + elif php_file.name.startswith("custom"): + custom_count += 1 + + return { + "enabled_sniff": ref, + "has_tests": bool(directories), + "fixture_count": len(fixture_paths), + "has_good": has_good, + "has_bad": has_bad, + "custom_count": custom_count, + "directories": ";".join(str(path.relative_to(ROOT)) for path in sorted(directories)), + } + + +def main() -> None: + enabled = run_phpcs_enabled() + refs = build_ref_directory_map() + + REPORTS.mkdir(parents=True, exist_ok=True) + csv_path = REPORTS / "sniff-coverage-matrix.csv" + md_path = REPORTS / "sniff-coverage-summary.md" + + rows = [summarize_sniff(ref, refs.get(ref, set())) for ref in enabled] + + with csv_path.open("w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter( + handle, + fieldnames=[ + "enabled_sniff", + "has_tests", + "fixture_count", + "has_good", + "has_bad", + "custom_count", + "directories", + ], + ) + writer.writeheader() + writer.writerows(rows) + + total = len(rows) + tested = sum(1 for row in rows if row["has_tests"]) + missing = [row for row in rows if not row["has_tests"]] + weak = [ + row + for row in rows + if row["has_tests"] and (not row["has_good"] or not row["has_bad"] or row["fixture_count"] == 1) + ] + strong = [ + row + for row in rows + if row["has_tests"] and row["has_good"] and row["has_bad"] and row["custom_count"] > 0 + ] + + with md_path.open("w", encoding="utf-8") as handle: + handle.write("# Sniff Coverage Summary\n\n") + handle.write(f"- Total enabled sniffs: {total}\n") + handle.write(f"- With dedicated tests: {tested}\n") + handle.write(f"- Missing dedicated tests: {len(missing)}\n") + handle.write(f"- Weak examples: {len(weak)}\n") + handle.write(f"- Strong examples (good+bad+custom): {len(strong)}\n\n") + + if missing: + handle.write("## Missing\n") + for row in missing: + handle.write(f"- `{row['enabled_sniff']}`\n") + handle.write("\n") + + if weak: + handle.write("## Weak Examples\n") + for row in weak: + handle.write( + f"- `{row['enabled_sniff']}` (fixtures={row['fixture_count']}, good={row['has_good']}, bad={row['has_bad']})\n" + ) + handle.write("\n") + + print(f"Generated {csv_path}") + print(f"Generated {md_path}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 6cb29a7..fdaffc3 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -63,12 +63,23 @@ jobs: - name: "Build ruleset diff report" run: bash .dev/ruleset-diff-report.sh + - name: "Build sniff coverage matrix" + run: python3 .dev/sniff-coverage-matrix.py + - name: "Upload ruleset diff report" uses: actions/upload-artifact@v4 with: name: ruleset-diff-report path: .reports/ruleset-diff.md + - name: "Upload sniff coverage matrix" + uses: actions/upload-artifact@v4 + with: + name: sniff-coverage-matrix + path: | + .reports/sniff-coverage-matrix.csv + .reports/sniff-coverage-summary.md + - name: "Regenerate snapshots" run: php bin/snapshots From ba3a7eab5f3a97da4b10171b25de6e0c1f53a30b Mon Sep 17 00:00:00 2001 From: OhMyFelix Date: Mon, 23 Feb 2026 10:16:25 +0000 Subject: [PATCH 7/7] CI: switch sniff coverage matrix generator to PHP --- .dev/sniff-coverage-matrix.php | 165 +++++++++++++++++++++++++++++++++ .dev/sniff-coverage-matrix.py | 144 ---------------------------- .github/workflows/tests.yaml | 2 +- 3 files changed, 166 insertions(+), 145 deletions(-) create mode 100644 .dev/sniff-coverage-matrix.php delete mode 100644 .dev/sniff-coverage-matrix.py diff --git a/.dev/sniff-coverage-matrix.php b/.dev/sniff-coverage-matrix.php new file mode 100644 index 0000000..a2a70c4 --- /dev/null +++ b/.dev/sniff-coverage-matrix.php @@ -0,0 +1,165 @@ +#!/usr/bin/env php +&1', $phpcsOutput, $exitCode); + +if ($exitCode !== 0) { + fwrite(STDERR, "Failed to list enabled sniffs.\n"); + fwrite(STDERR, implode("\n", $phpcsOutput) . "\n"); + exit($exitCode); +} + +$enabledSniffs = []; +foreach ($phpcsOutput as $line) { + if (preg_match('#^\s*([A-Z][A-Za-z0-9]*\.[A-Za-z0-9_]+\.[A-Za-z0-9_]+)\s*$#', $line, $matches) === 1) { + $enabledSniffs[$matches[1]] = true; + } +} +$enabledSniffs = array_keys($enabledSniffs); +sort($enabledSniffs); + +$refDirectories = []; +$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($sniffsRoot)); +foreach ($iterator as $file) { + if (!$file->isFile() || !str_ends_with($file->getFilename(), '.ruleset.xml')) { + continue; + } + + $content = file_get_contents($file->getPathname()); + if ($content === false) { + continue; + } + + if (preg_match_all('#getPath()] = true; + } + } +} + +foreach ($refDirectories as &$directories) { + $directories = array_keys($directories); + sort($directories); +} +unset($directories); + +if (!is_dir($reportsDir)) { + mkdir($reportsDir, 0777, true); +} + +$csvPath = $reportsDir . '/sniff-coverage-matrix.csv'; +$summaryPath = $reportsDir . '/sniff-coverage-summary.md'; + +$rows = []; +foreach ($enabledSniffs as $sniff) { + $directories = $refDirectories[$sniff] ?? []; + $fixtures = []; + $hasGood = false; + $hasBad = false; + $customCount = 0; + + foreach ($directories as $directory) { + $phpFiles = glob($directory . '/*.php') ?: []; + foreach ($phpFiles as $phpFile) { + $fixtures[$phpFile] = true; + $fileName = basename($phpFile); + if ($fileName === 'good.php') { + $hasGood = true; + } elseif ($fileName === 'bad.php') { + $hasBad = true; + } elseif (str_starts_with($fileName, 'custom')) { + $customCount++; + } + } + } + + $rows[] = [ + 'enabled_sniff' => $sniff, + 'has_tests' => count($directories) > 0, + 'fixture_count' => count($fixtures), + 'has_good' => $hasGood, + 'has_bad' => $hasBad, + 'custom_count' => $customCount, + 'directories' => implode(';', array_map(static fn(string $path): string => ltrim(str_replace($root, '', $path), '/'), $directories)), + ]; +} + +$csv = fopen($csvPath, 'wb'); +if ($csv === false) { + fwrite(STDERR, "Cannot open CSV report for writing: {$csvPath}\n"); + exit(1); +} + +fputcsv($csv, ['enabled_sniff', 'has_tests', 'fixture_count', 'has_good', 'has_bad', 'custom_count', 'directories']); +foreach ($rows as $row) { + fputcsv($csv, [ + $row['enabled_sniff'], + $row['has_tests'] ? 'true' : 'false', + (string) $row['fixture_count'], + $row['has_good'] ? 'true' : 'false', + $row['has_bad'] ? 'true' : 'false', + (string) $row['custom_count'], + $row['directories'], + ]); +} +fclose($csv); + +$total = count($rows); +$tested = count(array_filter($rows, static fn(array $row): bool => $row['has_tests'] === true)); +$missing = array_values(array_filter($rows, static fn(array $row): bool => $row['has_tests'] === false)); +$weak = array_values(array_filter( + $rows, + static fn(array $row): bool => $row['has_tests'] === true && (!$row['has_good'] || !$row['has_bad'] || $row['fixture_count'] === 1) +)); +$strong = count(array_filter( + $rows, + static fn(array $row): bool => $row['has_tests'] === true && $row['has_good'] && $row['has_bad'] && $row['custom_count'] > 0 +)); + +$summary = []; +$summary[] = '# Sniff Coverage Summary'; +$summary[] = ''; +$summary[] = '- Total enabled sniffs: ' . $total; +$summary[] = '- With dedicated tests: ' . $tested; +$summary[] = '- Missing dedicated tests: ' . count($missing); +$summary[] = '- Weak examples: ' . count($weak); +$summary[] = '- Strong examples (good+bad+custom): ' . $strong; +$summary[] = ''; + +if ($missing !== []) { + $summary[] = '## Missing'; + foreach ($missing as $row) { + $summary[] = '- `' . $row['enabled_sniff'] . '`'; + } + $summary[] = ''; +} + +if ($weak !== []) { + $summary[] = '## Weak Examples'; + foreach ($weak as $row) { + $summary[] = sprintf( + '- `%s` (fixtures=%d, good=%s, bad=%s)', + $row['enabled_sniff'], + $row['fixture_count'], + $row['has_good'] ? 'true' : 'false', + $row['has_bad'] ? 'true' : 'false' + ); + } + $summary[] = ''; +} + +file_put_contents($summaryPath, implode("\n", $summary)); + +fwrite(STDOUT, "Generated {$csvPath}\n"); +fwrite(STDOUT, "Generated {$summaryPath}\n"); diff --git a/.dev/sniff-coverage-matrix.py b/.dev/sniff-coverage-matrix.py deleted file mode 100644 index bf4aae3..0000000 --- a/.dev/sniff-coverage-matrix.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import csv -import re -import subprocess -from collections import defaultdict -from pathlib import Path - - -ROOT = Path(__file__).resolve().parent.parent -REPORTS = ROOT / ".reports" -SNIFFS_ROOT = ROOT / "tests" / "Sniffs" - -SNIFF_RE = re.compile(r"^\s*([A-Z][A-Za-z0-9]*\.[A-Za-z0-9_]+\.[A-Za-z0-9_]+)\s*$") -RULE_REF_RE = re.compile(r" list[str]: - result = subprocess.run( - ["vendor/bin/phpcs", "--standard=ruleset.xml", "-e"], - cwd=ROOT, - text=True, - capture_output=True, - check=True, - ) - - sniffs: list[str] = [] - for line in result.stdout.splitlines(): - match = SNIFF_RE.match(line) - if match: - sniffs.append(match.group(1)) - - return sorted(set(sniffs)) - - -def build_ref_directory_map() -> dict[str, set[Path]]: - refs: dict[str, set[Path]] = defaultdict(set) - - for ruleset in SNIFFS_ROOT.glob("**/*.ruleset.xml"): - content = ruleset.read_text(encoding="utf-8") - for ref in RULE_REF_RE.findall(content): - if "." in ref: - refs[ref].add(ruleset.parent) - - return refs - - -def summarize_sniff(ref: str, directories: set[Path]) -> dict[str, object]: - fixture_paths: set[Path] = set() - has_good = False - has_bad = False - custom_count = 0 - - for directory in sorted(directories): - for php_file in directory.glob("*.php"): - fixture_paths.add(php_file) - if php_file.name == "good.php": - has_good = True - elif php_file.name == "bad.php": - has_bad = True - elif php_file.name.startswith("custom"): - custom_count += 1 - - return { - "enabled_sniff": ref, - "has_tests": bool(directories), - "fixture_count": len(fixture_paths), - "has_good": has_good, - "has_bad": has_bad, - "custom_count": custom_count, - "directories": ";".join(str(path.relative_to(ROOT)) for path in sorted(directories)), - } - - -def main() -> None: - enabled = run_phpcs_enabled() - refs = build_ref_directory_map() - - REPORTS.mkdir(parents=True, exist_ok=True) - csv_path = REPORTS / "sniff-coverage-matrix.csv" - md_path = REPORTS / "sniff-coverage-summary.md" - - rows = [summarize_sniff(ref, refs.get(ref, set())) for ref in enabled] - - with csv_path.open("w", newline="", encoding="utf-8") as handle: - writer = csv.DictWriter( - handle, - fieldnames=[ - "enabled_sniff", - "has_tests", - "fixture_count", - "has_good", - "has_bad", - "custom_count", - "directories", - ], - ) - writer.writeheader() - writer.writerows(rows) - - total = len(rows) - tested = sum(1 for row in rows if row["has_tests"]) - missing = [row for row in rows if not row["has_tests"]] - weak = [ - row - for row in rows - if row["has_tests"] and (not row["has_good"] or not row["has_bad"] or row["fixture_count"] == 1) - ] - strong = [ - row - for row in rows - if row["has_tests"] and row["has_good"] and row["has_bad"] and row["custom_count"] > 0 - ] - - with md_path.open("w", encoding="utf-8") as handle: - handle.write("# Sniff Coverage Summary\n\n") - handle.write(f"- Total enabled sniffs: {total}\n") - handle.write(f"- With dedicated tests: {tested}\n") - handle.write(f"- Missing dedicated tests: {len(missing)}\n") - handle.write(f"- Weak examples: {len(weak)}\n") - handle.write(f"- Strong examples (good+bad+custom): {len(strong)}\n\n") - - if missing: - handle.write("## Missing\n") - for row in missing: - handle.write(f"- `{row['enabled_sniff']}`\n") - handle.write("\n") - - if weak: - handle.write("## Weak Examples\n") - for row in weak: - handle.write( - f"- `{row['enabled_sniff']}` (fixtures={row['fixture_count']}, good={row['has_good']}, bad={row['has_bad']})\n" - ) - handle.write("\n") - - print(f"Generated {csv_path}") - print(f"Generated {md_path}") - - -if __name__ == "__main__": - main() diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index fdaffc3..f2d66e2 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -64,7 +64,7 @@ jobs: run: bash .dev/ruleset-diff-report.sh - name: "Build sniff coverage matrix" - run: python3 .dev/sniff-coverage-matrix.py + run: php .dev/sniff-coverage-matrix.php - name: "Upload ruleset diff report" uses: actions/upload-artifact@v4