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/.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/.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..f2d66e2 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,4 +1,4 @@ -name: "Nette Tester" +name: "Tests" on: pull_request: @@ -11,32 +11,77 @@ 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" 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: "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: "Build sniff coverage matrix" + run: php .dev/sniff-coverage-matrix.php + + - 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 + + - 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/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']); + } + +} 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." + } + ] + } + } +} 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,