diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2900f95 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,97 @@ +name: Test + +on: + # Run on pushes to `master` and on all pull requests. + # Prevent the "push" build from running when there are only irrelevant changes. + push: + branches: + - master + paths-ignore: + - '**.md' + pull_request: + + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + checkxml: + name: 'Check XML Validates' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install xmllint + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y libxml2-utils + + # Show XML violations inline in the file diff. + # @link https://github.com/marketplace/actions/xmllint-problem-matcher + - uses: korelstar/xmllint-problem-matcher@v1 + + - name: 'Validate XML against schema and check code style' + run: ./bin/xml-lint + + test: + runs-on: ubuntu-latest + + strategy: + # Keys: + # - php: The PHP versions to test against. + # - dependencies: The PHPCS dependencies versions to test against. + # IMPORTANT: test runs shouldn't fail because of PHPCS being incompatible with a PHP version. + # - PHPCS will run without errors on PHP 5.4 - 7.4 on any supported version. + # - PHP 8.0 needs PHPCS 3.5.7+ to run without errors, and we require a higher minimum version. + # - PHP 8.1 needs PHPCS 3.6.1+ to run without errors, but works best with 3.7.1+, and we require at least this minimum version. + matrix: + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + dependencies: ['stable'] + name: "Test: PHP ${{ matrix.php }} - PHPCS" + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + # With stable PHPCS dependencies, allow for PHP deprecation notices. + # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. + - name: Setup ini config + id: set_ini + run: | + echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> $GITHUB_OUTPUT + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: ${{ steps.set_ini.outputs.PHP_INI }} + coverage: none + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-composer-dependencies + - name: Install Composer dependencies - normal + if: ${{ startsWith( matrix.php, '8' ) == false }} + uses: "ramsey/composer-install@v2" + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + # PHPUnit 7.x does not allow for installation on PHP 8, so ignore platform + # requirements to get PHPUnit 7.x to install on nightly. + - name: Install Composer dependencies - with ignore platform + if: ${{ startsWith( matrix.php, '8' ) }} + uses: "ramsey/composer-install@v2" + with: + composer-options: --ignore-platform-req=php+ + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Run the ruleset tests + run: ./bin/ruleset-tests diff --git a/10up-Default/ruleset-test.inc b/10up-Default/ruleset-test.inc new file mode 100644 index 0000000..278759e --- /dev/null +++ b/10up-Default/ruleset-test.inc @@ -0,0 +1,486 @@ + + + + 999, +); + +// Warnings: WordPress.WP.PostsPerPage.posts_per_page_posts_per_page +_query_posts( 'posts_per_page=999' ); + +// Warnings: WordPress.WP.PostsPerPage.posts_per_page_posts_per_page +$query_args['posts_per_page'] = 999; + +// Errors: WordPress.DateTime.RestrictedFunctions.timezone_change_date_default_timezone_set) +date_default_timezone_set( 'FooBar' ); + +$b = function () { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + global $wpdb; + $listofthings = wp_cache_get( 'foo' ); + if ( ! $listofthings ) { + $foo = "column = 'test'"; + + // Errors: WordPress.DB.PreparedSQL.NotPrepared + // Warnings: WordPress.DB.DirectDatabaseQuery.DirectQuery + $listofthings = $wpdb->query( 'SELECT something FROM somewhere WHERE ' . $foo ); + wp_cache_set( 'foo', $listofthings ); + } +}; + +// Warnings: WordPress.DB.DirectDatabaseQuery.DirectQuery & WordPress.DB.PreparedSQLPlaceholders.UnnecessaryPrepare & WordPress.DB.DirectDatabaseQuery.NoCaching +$baz = $wpdb->get_results( $wpdb->prepare( 'SELECT X FROM Y ' ) ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Warning x 2. + +$test = [ + // Warnings: WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'tax_query' => [], +]; + +// Errors: PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket +new WP_Query( array( + // Warnings: WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => [], + // Warnings: WordPress.DB.SlowDBQuery.slow_db_query_meta_key & WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned + 'meta_key' => 'foo', + // Warnings: Warnings: WordPress.DB.SlowDBQuery.slow_db_query_meta_value + 'meta_value' => 'bar', + // Errors: PEAR.Functions.FunctionCallSignature.CloseBracketLine +) ); + +// WordPress.WP.GlobalVariablesOverride +$GLOBALS['wpdb'] = 'test'; + +// Errors: Generic.CodeAnalysis.EmptyStatement.DetectedIf +// Warnings: Universal.Operators.StrictComparisons.LooseEqual +if ( true == $true ) { +} + +// Errors: Generic.CodeAnalysis.EmptyStatement.DetectedIf & Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure +// Warnings: Generic.CodeAnalysis.AssignmentInCondition.Found +if ( $test = get_post( $post ) ) { +} + +// Errors: Generic.CodeAnalysis.EmptyStatement.DetectedIf) +// Warnings: WordPress.PHP.StrictInArray.MissingTrueStrict +if ( true === in_array( $foo, $bar ) ) { +} + +// Errors: WordPress.PHP.DontExtract.extract_extract +extract( $foobar ); + +// WordPress.WP.CronInterval +function my_add_weekly( $schedules ) { + $schedules['every_6_mins'] = array( + 'interval' => 360, + // Errors: NormalizedArrays.Arrays.CommaAfterLast.MissingMultiLine + // Warnings: WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned + 'display' => __( 'Once every 6 minutes' ) + ); + return $schedules; +} + +// Errors: PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket +// Warnings:WordPress.WP.CronInterval.CronSchedulesInterval +add_filter( 'cron_schedules', 'my_add_weekly'); + +// Errors: Universal.Files.SeparateFunctionsFromOO.Mixed +class TestClass extends MyClass +// Errors: Generic.Classes.OpeningBraceSameLine.BraceOnNewLine +{ + // Errors: Squiz.Scope.MethodScope.Missing + function __construct() { + parent::MYCLASS(); + parent::__construct(); + } +} + +// Errors: Generic.Files.OneObjectStructurePerFile.MultipleFound +class OldClass +// Errors: Generic.Classes.OpeningBraceSameLine.BraceOnNewLine +{ + + // Errors: Squiz.Scope.MethodScope.Missing + function OldClass() + // Errors: (Generic.Functions.OpeningFunctionBraceKernighanRitchie.BraceOnNewLine + { + } +} + +// Errors: Generic.Files.OneObjectStructurePerFile.MultipleFound +// Warnings: Generic.Classes.DuplicateClassName.Found +class TestClass extends MyClass { + + // Errors: Squiz.Scope.MethodScope.Missing + function TestClass() { + parent::MyClass(); + parent::__construct(); + } +} + +// Errors: Squiz.PHP.EmbeddedPhp.ContentAfterEnd & Generic.PHP.DisallowShortOpenTag.EchoFound +?> +// if (empty($this)) {echo 'This is will not work';} + +// Errors: PEAR.Functions.FunctionCallSignature.SpaceAfterOpenBracket & Squiz.PHP.Eval.Discouraged +eval('$var = 4;'); // Error + Message. + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode +base64_decode( 'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==' ); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode +base64_encode( 'This is an encoded string' ); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.obfuscation_convert_uudecode +convert_uudecode( "+22!L;W9E(%!(4\"$`\n`" ); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.obfuscation_convert_uudecode +convert_uuencode( "test\ntext text\r\n" ); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.obfuscation_str_rot13 +str_rot13( 'The quick brown fox jumps over the lazy dog.' ); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize +serialize(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize +unserialize(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode +urlencode(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.system_calls_passthru +passthru( 'cat myfile.zip', $err ); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.system_calls_proc_open +$process = proc_open( 'php', $descriptorspec, $pipes, $cwd, $env ); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.system_calls_system +$last_line = system( 'ls', $retval ); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.system_calls_popen +$handle = popen( '/bin/ls', 'r' ); + +// Warnings: WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting & WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting +error_reporting(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_ini_restore +ini_restore(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv +apache_setenv(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv +putenv(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_set_include_path +set_include_path(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_restore_include_path && PHPCompatibility.FunctionUse.RemovedFunctions.restore_include_pathDeprecated +restore_include_path(); + +// Errors: PHPCompatibility.FunctionUse.RemovedFunctions.magic_quotes_runtimeDeprecatedRemoved +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_magic_quotes_runtime +magic_quotes_runtime(); + +// Errors: PHPCompatibility.FunctionUse.RemovedFunctions.set_magic_quotes_runtimeDeprecatedRemoved +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_set_magic_quotes_runtime +set_magic_quotes_runtime(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_dl & PHPCompatibility.FunctionUse.RemovedFunctions.dlDeprecated +dl(); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec +exec( 'whoami' ); + +// Warnings: WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec +$output = shell_exec( 'ls -lart' ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Error. + +// Warnings: WordPress.PHP.DevelopmentFunctions.error_log_var_dump +var_dump(); + +// Warnings: WordPress.PHP.DevelopmentFunctions.error_log_var_export +var_export(); + +// Warnings: WordPress.PHP.DevelopmentFunctions.error_log_print_r +print_r(); + +// Warnings: WordPress.PHP.DevelopmentFunctions.error_log_trigger_error +trigger_error( 'message' ); + +// Warnings: WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler +set_error_handler(); + +// Warnings: WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace +debug_backtrace(); + +// Warnings: WordPress.PHP.DevelopmentFunctions.error_log_debug_print_backtrace +debug_print_backtrace(); + +// Warnings: WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary +wp_debug_backtrace_summary(); + +// Warnings: WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_phpinfo +phpinfo(); + +// Warnings: WordPress.PHP.DevelopmentFunctions.error_log_error_log +error_log(); + +// OK to set +ini_set( 'auto_detect_line_endings', true ); + +// Errors: PHPCompatibility.IniDirectives.RemovedIniDirectives.highlight_bgDeprecatedRemoved +ini_set( 'highlight.bg', '#000000' ); + +// OK to set +ini_set( 'highlight.comment', '#000000' ); + +// OK to set +ini_set( 'highlight.default', '#000000' ); + +// OK to set +ini_set( 'highlight.html', '#000000' ); + +// OK to set +ini_set( 'highlight.keyword', '#000000' ); + +// OK to set +ini_set( 'highlight.string', '#000000' ); + +// OK to set +ini_set( 'short_open_tag', 1 ); + +// Errors: WordPress.PHP.IniSet.bcmath_scale_Disallowed +ini_set( 'bcmath.scale', 1 ); + +// Errors: WordPress.PHP.IniSet.display_errors_Disallowed +ini_set( 'display_errors', 1 ); + +// Errors: WordPress.PHP.IniSet.error_reporting_Disallowed +ini_set( 'error_reporting', 1 ); + +// Errors: WordPress.PHP.IniSet.filter_default_Disallowed +ini_set( 'filter.default', 1 ); + +// Errors: WordPress.PHP.IniSet.filter_default_flags_Disallowed +ini_set( 'filter.default_flags', 1 ); + +// Errors: WordPress.PHP.IniSet.iconv_input_encoding_Disallowed +// Warnings: PHPCompatibility.IniDirectives.RemovedIniDirectives.iconv_input_encodingDeprecated +ini_set( 'iconv.input_encoding', 1 ); + +// Errors: WordPress.PHP.IniSet.iconv_internal_encoding_Disallowed +// Warnings: PHPCompatibility.IniDirectives.RemovedIniDirectives.iconv_internal_encodingDeprecated +ini_set( 'iconv.internal_encoding', 1 ); + +// Errors: WordPress.PHP.IniSet.iconv_output_encoding_Disallowed +// Warnings: PHPCompatibility.IniDirectives.RemovedIniDirectives.iconv_output_encodingDeprecated +ini_set( 'iconv.output_encoding', 1 ); + +// Errors: WordPress.PHP.IniSet.ignore_user_abort_Disallowed +ini_set( 'ignore_user_abort', 1 ); + +// Errors: WordPress.PHP.IniSet.log_errors_Disallowed +ini_set( 'log_errors', 1 ); + +// Errors: WordPress.PHP.IniSet.max_execution_time_Disallowed +ini_set( 'max_execution_time', 1 ); + +// Errors: WordPress.PHP.IniSet.memory_limit_Disallowed +ini_set( 'memory_limit', 1 ); + +// Errors: WordPress.PHP.IniSet.short_open_tag_Disallowed +ini_set( 'short_open_tag', 'off' ); + +// Warnings: WordPress.PHP.IniSet.Risky +ini_set( 'foo', true ); + +// OK to set +ini_alter( 'auto_detect_line_endings', true ); + +// OK to set +ini_alter( 'highlight.bg', '#000000' ); + +// OK to set +ini_alter( 'highlight.comment', '#000000' ); + +// OK to set +ini_alter( 'highlight.default', '#000000' ); + +// OK to set +ini_alter( 'highlight.html', '#000000' ); + +// OK to set +ini_alter( 'highlight.keyword', '#000000' ); + +// OK to set +ini_alter( 'highlight.string', '#000000' ); + +// OK to set +ini_alter( 'short_open_tag', 1 ); + +// Errors: WordPress.PHP.IniSet.bcmath_scale_Disallowed +ini_alter( 'bcmath.scale', 1 ); + +// Errors: WordPress.PHP.IniSet.display_errors_Disallowed +ini_alter( 'display_errors', 1 ); + +// Errors: WordPress.PHP.IniSet.error_reporting_Disallowed +ini_alter( 'error_reporting', 1 ); + +// Errors: WordPress.PHP.IniSet.filter_default_Disallowed +ini_alter( 'filter.default', 1 ); + +// Errors: WordPress.PHP.IniSet.filter_default_flags_Disallowed +ini_alter( 'filter.default_flags', 1 ); + +// Errors: WordPress.PHP.IniSet.iconv_input_encoding_Disallowed +ini_alter( 'iconv.input_encoding', 1 ); + +// Errors: WordPress.PHP.IniSet.iconv_internal_encoding_Disallowed +ini_alter( 'iconv.internal_encoding', 1 ); + +// Errors: WordPress.PHP.IniSet.iconv_output_encoding_Disallowed +ini_alter( 'iconv.output_encoding', 1 ); + +// Errors: WordPress.PHP.IniSet.ignore_user_abort_Disallowed +ini_alter( 'ignore_user_abort', 1 ); + +// Errors: WordPress.PHP.IniSet.log_errors_Disallowed +ini_alter( 'log_errors', 1 ); + +// Errors: WordPress.PHP.IniSet.max_execution_time_Disallowed +ini_alter( 'max_execution_time', 1 ); + +// Errors: WordPress.PHP.IniSet.memory_limit_Disallowed +ini_alter( 'memory_limit', 1 ); + +// Errors: WordPress.PHP.IniSet.short_open_tag_Disallowed +ini_alter( 'short_open_tag', 'off' ); + +// Warnings: WordPress.PHP.IniSet.Risky +ini_alter( 'foo', true ); + +// Warnings: WordPress.WP.AlternativeFunctions.curl_curl_init +curl_init(); + +// Warnings: WordPress.WP.AlternativeFunctions.curl_curl_close +curl_close( $ch ); + +// Warnings: WordPress.WP.AlternativeFunctions.curl_curl_getinfo +CURL_getinfo(); + +// Warnings: WordPress.WP.AlternativeFunctions.parse_url_parse_url +parse_url( 'http://example.com/' ); + +// Warnings: WordPress.WP.AlternativeFunctions.json_encode_json_encode +$json = json_encode( $thing ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Warning. + +// Warnings: WordPress.WP.AlternativeFunctions.file_system_operations_readfile +readfile(); + +// Warnings: WordPress.WP.AlternativeFunctions.file_system_operations_fclose +fclose(); + +// Warnings: WordPress.WP.AlternativeFunctions.file_system_operations_fopen +fopen(); + +// Warnings: WordPress.WP.AlternativeFunctions.file_system_operations_fread +fread(); + +// Warnings: WordPress.WP.AlternativeFunctions.file_system_operations_fsockopen +fsockopen(); + +// Warnings: WordPress.WP.AlternativeFunctions.file_system_operations_pfsockopen +pfsockopen(); + +// Warnings: WordPress.WP.AlternativeFunctions.rand_seeding_srand +srand(); + +// Warnings: WordPress.WP.AlternativeFunctions.rand_seeding_mt_srand +mt_srand(); + +// Warnings: WordPress.WP.AlternativeFunctions.rand_rand +rand(); + +// Warnings: WordPress.WP.AlternativeFunctions.rand_mt_rand +mt_rand(); + +// Errors: Generic.Files.OneObjectStructurePerFile.MultipleFound +class MyClass { + + // Errors: Squiz.Scope.MethodScope.Missing + function my_function() { + // Errors: Squiz.Functions.MultiLineFunctionDeclaration.SpaceAfterFunction + return function() { + $this->my_callback(); // OK - new VariableAnalysis doesn't flag $this as undefined in closure. + }; + } + + // Errors: Squiz.Scope.MethodScope.Missing + function my_callback() {} +} + +// Errors: Generic.VersionControl.GitMergeConflict.OpenerFound +?> +<<<<<<< HEAD + +>>>>>>> + + + + [ + 4 => 1, + 5 => 1, + 6 => 41, + 10 => 5, + 15 => 2, + 20 => 1, + 22 => 1, + 26 => 1, + 31 => 1, + 33 => 3, + 35 => 1, + 40 => 1, + 43 => 2, + 57 => 1, + 67 => 1, + 81 => 1, + 89 => 1, + 96 => 1, + 101 => 2, + 106 => 1, + 110 => 1, + 118 => 1, + 125 => 1, + 128 => 1, + 130 => 1, + 132 => 1, + 139 => 1, + 141 => 1, + 144 => 1, + 146 => 1, + 152 => 1, + 155 => 1, + 162 => 2, + 168 => 3, + 222 => 1, + 226 => 1, + 230 => 1, + 275 => 1, + 296 => 1, + 299 => 1, + 302 => 1, + 305 => 1, + 308 => 1, + 312 => 1, + 316 => 1, + 320 => 1, + 323 => 1, + 326 => 1, + 329 => 1, + 332 => 1, + 335 => 1, + 365 => 1, + 368 => 1, + 371 => 1, + 374 => 1, + 377 => 1, + 380 => 1, + 383 => 1, + 386 => 1, + 389 => 1, + 392 => 1, + 395 => 1, + 398 => 1, + 401 => 1, + 452 => 1, + 455 => 1, + 457 => 1, + 463 => 1, + 468 => 1, + 475 => 1, + 478 => 1, + 483 => 4, + 486 => 1, + ], + 'warnings' => [ + 15 => 1, + 40 => 2, + 47 => 1, + 51 => 1, + 54 => 1, + 67 => 1, + 73 => 3, + 77 => 1, + 83 => 1, + 85 => 2, + 87 => 1, + 96 => 1, + 101 => 1, + 106 => 1, + 118 => 1, + 125 => 1, + 152 => 1, + 171 => 1, + 174 => 1, + 177 => 1, + 180 => 1, + 183 => 1, + 186 => 1, + 189 => 1, + 192 => 1, + 195 => 1, + 198 => 1, + 201 => 1, + 204 => 1, + 207 => 2, + 210 => 1, + 213 => 1, + 216 => 1, + 219 => 1, + 222 => 1, + 226 => 1, + 230 => 1, + 233 => 2, + 236 => 1, + 239 => 1, + 242 => 1, + 245 => 1, + 248 => 1, + 251 => 1, + 254 => 1, + 257 => 1, + 260 => 1, + 263 => 1, + 266 => 1, + 269 => 1, + 272 => 1, + 305 => 1, + 312 => 1, + 316 => 1, + 320 => 1, + 338 => 1, + 404 => 1, + 407 => 1, + 410 => 1, + 413 => 1, + 416 => 1, + 419 => 1, + 422 => 1, + 425 => 1, + 428 => 1, + 431 => 1, + 434 => 1, + 437 => 1, + 440 => 1, + 443 => 1, + 446 => 1, + 449 => 1, + 475 => 1, + 486 => 1, + ], + 'messages' => [], +]; + +// If we're running on PHP 7.4, we need to account for the error thrown because `restore_include_path()` is deprecated. +// See https://www.php.net/manual/en/function.restore-include-path.php +if ( version_compare( PHP_VERSION, '7.4.0', '>=' ) && version_compare( PHP_VERSION, '8.0.0', '<' ) ) { + $expected['errors'][ 222 ] = 2; +} + +// We have some specific errors that are only thrown on PHP 8.2+. +if ( version_compare( PHP_VERSION, '8.2.0', '<' ) ) { + $expected['errors'][ 486 ] = 0; +} + +if ( version_compare( PHP_VERSION, '7.2', '<' ) ) { + $expected['errors'][ 483 ] = 4; + $expected['warnings'][ 1 ] = 1; +} + +require __DIR__ . '/../tests/RulesetTest.php'; + +// Run the tests! +$test = new \PhpcsComposer\RulesetTest( 'TenUpDefault', $expected ); +if ( $test->passes() ) { + printf( 'All TenUpDefault tests passed!' . PHP_EOL ); + exit( 0 ); +} + +exit( 1 ); diff --git a/10up-Default/ruleset.xml b/10up-Default/ruleset.xml index d2bfdaa..f78578a 100644 --- a/10up-Default/ruleset.xml +++ b/10up-Default/ruleset.xml @@ -51,7 +51,7 @@ - + @@ -70,7 +70,7 @@ - + @@ -79,9 +79,8 @@ - - - + + warning @@ -103,14 +102,14 @@ - + - + - + diff --git a/README.md b/README.md index ef2d992..f4ac47f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Install the library via Composer: ```bash -$ composer require --dev 10up/phpcs-composer:dev-master +$ composer require --dev 10up/phpcs-composer:"~3" ``` That's it! @@ -40,7 +40,7 @@ $ composer run lint ### Continuous Integration -PHPCS Configuration plays nicely with Continuous Integration solutions. Out of the box, the library loads the `10up-Default` ruleset, and checks for syntax errors for PHP 7 or higher. +PHPCS Configuration plays nicely with Continuous Integration solutions. Out of the box, the library loads the `10up-Default` ruleset, and checks for syntax errors for PHP 8.2 or higher. To override the default PHP version check, set the `--runtime-set testVersion 7.0-` configuration option. Example for PHP version 7.2 and above: diff --git a/bin/ruleset-tests b/bin/ruleset-tests new file mode 100755 index 0000000..59125a3 --- /dev/null +++ b/bin/ruleset-tests @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# +# Run ruleset tests. +# +# This will check that each ruleset correctly includes the expected sniffs from this and other packages. If a sniff +# reference has changed (moved category, or been renamed, etc.) then the expected errors, warnings, and messages will +# not match what the output is, when the ruleset-test.inc is checked with PHPCS against the relevant ruleset. +# +# To run the tests, make sure you have the PHPCS, including the TenUpDefault standard, installed and executable +# * using the `phpcs --standard=TenUpDefault` command. +# +# From the root of this VIP-Coding-Standards package, you can then run: +# +# ./bin/ruleset-tests + +# Set PHPCS_BIN, which is used in the tests/RulesetTest.php files. +PHPCS_BIN="$(pwd)/vendor/bin/phpcs" +export PHPCS_BIN + +php ./10up-Default/ruleset-test.php diff --git a/bin/xml-lint b/bin/xml-lint new file mode 100755 index 0000000..8ea2980 --- /dev/null +++ b/bin/xml-lint @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# +# Check XML files. +# +# @link http://xmlsoft.org/xmllint.html +# +# EXAMPLE TO RUN LOCALLY: +# +# ./bin/xml-lint + +# Validate the ruleset XML files. +xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./*/ruleset.xml + +# Check the code-style consistency of the XML files. +export XMLLINT_INDENT=" " # This is a tab character. +diff -B --tabsize=4 ./10up-Default/ruleset.xml <(xmllint --format "./10up-Default/ruleset.xml") diff --git a/composer.json b/composer.json index 4c40963..4136644 100644 --- a/composer.json +++ b/composer.json @@ -1,34 +1,36 @@ { - "name": "10up/phpcs-composer", - "description": "10up's PHP CodeSniffer Ruleset", - "type": "phpcodesniffer-standard", - "license": "MIT", - "require": { - "dealerdirect/phpcodesniffer-composer-installer": "*", - "phpcompatibility/phpcompatibility-wp": "^2", - "squizlabs/php_codesniffer" : "3.7.1", - "wp-coding-standards/wpcs": "*", - "automattic/vipwpcs": "^2.3" - }, - "prefer-stable" : true, - "authors": [ - { - "name": "10up", - "homepage": "https://10up.com/" - } + "name": "10up/phpcs-composer", + "description": "10up's PHP CodeSniffer Ruleset", + "type": "phpcodesniffer-standard", + "license": "MIT", + "require": { + "automattic/vipwpcs": "^3.0", + "phpcompatibility/phpcompatibility-wp": "^2" + }, + "prefer-stable": true, + "authors": [ + { + "name": "10up", + "homepage": "https://10up.com/" + } + ], + "scripts": { + "config-cs": [ + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run", + "\"vendor/bin/phpcs\" --config-set default_standard 10up-Default" ], - "scripts": { - "config-cs": [ - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run", - "\"vendor/bin/phpcs\" --config-set default_standard 10up-Default" - ], - "post-install-cmd": "@config-cs", - "post-update-cmd": "@config-cs", - "lint": "\"vendor/bin/phpcs\" . " - }, - "config": { - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true - } + "post-install-cmd": "@config-cs", + "post-update-cmd": "@config-cs", + "lint": "\"vendor/bin/phpcs\" . " + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true } + }, + "minimum-stability": "dev", + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "*", + "phpcompatibility/php-compatibility": "dev-develop as 9.99.99" + } } diff --git a/composer.lock b/composer.lock index 5a6c6c0..85dc10b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,28 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5a64f044808a357a1ef6f92e99ffaca1", + "content-hash": "5a7e2b9a63bc91d26ccd8c8109b246e8", "packages": [ { "name": "automattic/vipwpcs", - "version": "2.3.4", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/Automattic/VIP-Coding-Standards.git", - "reference": "b8610e3837f49c5f2fcc4b663b6c0a7c9b3509b6" + "reference": "1b8960ebff9ea3eb482258a906ece4d1ee1e25fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/b8610e3837f49c5f2fcc4b663b6c0a7c9b3509b6", - "reference": "b8610e3837f49c5f2fcc4b663b6c0a7c9b3509b6", + "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/1b8960ebff9ea3eb482258a906ece4d1ee1e25fd", + "reference": "1b8960ebff9ea3eb482258a906ece4d1ee1e25fd", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", + "phpcsstandards/phpcsextra": "^1.1.0", + "phpcsstandards/phpcsutils": "^1.0.8", "sirbrillig/phpcs-variable-analysis": "^2.11.17", - "squizlabs/php_codesniffer": "^3.7.1", - "wp-coding-standards/wpcs": "^2.3" + "squizlabs/php_codesniffer": "^3.7.2", + "wp-coding-standards/wpcs": "^3.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", @@ -57,7 +58,7 @@ "source": "https://github.com/Automattic/VIP-Coding-Standards", "wiki": "https://github.com/Automattic/VIP-Coding-Standards/wiki" }, - "time": "2023-08-24T15:11:13+00:00" + "time": "2023-09-05T11:01:05+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -139,33 +140,45 @@ }, { "name": "phpcompatibility/php-compatibility", - "version": "9.3.5", + "version": "dev-develop", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + "reference": "8770f4f4677cc649a1df5e73dc6195d9887f4641" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/8770f4f4677cc649a1df5e73dc6195d9887f4641", + "reference": "8770f4f4677cc649a1df5e73dc6195d9887f4641", "shasum": "" }, "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.0.5", + "squizlabs/php_codesniffer": "^3.7.1" }, - "conflict": { - "squizlabs/php_codesniffer": "2.6.2" + "replace": { + "wimg/php-compatibility": "*" }, "require-dev": { - "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcsstandards/phpcsdevcs": "^1.1.3", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.1.0", + "yoast/phpunit-polyfills": "^1.0.5 || ^2.0.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, + "default-branch": true, "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev", + "dev-develop": "10.x-dev" + } + }, "notification-url": "https://packagist.org/downloads/", "license": [ "LGPL-3.0-or-later" @@ -191,13 +204,14 @@ "keywords": [ "compatibility", "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", "source": "https://github.com/PHPCompatibility/PHPCompatibility" }, - "time": "2019-12-27T09:44:58+00:00" + "time": "2023-11-13T01:28:23+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", @@ -311,6 +325,142 @@ }, "time": "2022-10-24T09:00:36+00:00" }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "746c3190ba8eb2f212087c947ba75f4f5b9a58d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/746c3190ba8eb2f212087c947ba75f4f5b9a58d5", + "reference": "746c3190ba8eb2f212087c947ba75f4f5b9a58d5", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.0.8", + "squizlabs/php_codesniffer": "^3.7.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcsstandards/phpcsdevcs": "^1.1.6", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "time": "2023-09-20T22:06:18+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "69465cab9d12454e5e7767b9041af0cd8cd13be7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/69465cab9d12454e5e7767b9041af0cd8cd13be7", + "reference": "69465cab9d12454e5e7767b9041af0cd8cd13be7", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.7.1 || 4.0.x-dev@dev" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcsstandards/phpcsdevcs": "^1.1.6", + "yoast/phpunit-polyfills": "^1.0.5 || ^2.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "time": "2023-07-16T21:39:41+00:00" + }, { "name": "sirbrillig/phpcs-variable-analysis", "version": "v2.11.17", @@ -371,16 +521,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.1", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -416,41 +566,50 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2022-06-18T07:21:10+00:00" + "time": "2023-02-22T23:07:41+00:00" }, { "name": "wp-coding-standards/wpcs", - "version": "2.3.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "7da1894633f168fe244afc6de00d141f27517b62" + "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", - "reference": "7da1894633f168fe244afc6de00d141f27517b62", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b4caf9689f1a0e4a4c632679a44e638c1c67aff1", + "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1", "shasum": "" }, "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.3.1" + "phpcsstandards/phpcsextra": "^1.1.0", + "phpcsstandards/phpcsutils": "^1.0.8", + "squizlabs/php_codesniffer": "^3.7.2" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcompatibility/php-compatibility": "^9.0", - "phpcsstandards/phpcsdevtools": "^1.0", + "phpcsstandards/phpcsdevtools": "^1.2.0", "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", @@ -467,6 +626,7 @@ "keywords": [ "phpcs", "standards", + "static analysis", "wordpress" ], "support": { @@ -474,13 +634,28 @@ "source": "https://github.com/WordPress/WordPress-Coding-Standards", "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" }, - "time": "2020-05-13T23:57:56+00:00" + "funding": [ + { + "url": "https://opencollective.com/thewpcc/contribute/wp-php-63406", + "type": "custom" + } + ], + "time": "2023-09-14T07:06:09+00:00" } ], "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], + "aliases": [ + { + "package": "phpcompatibility/php-compatibility", + "version": "dev-develop", + "alias": "9.99.99", + "alias_normalized": "9.99.99.0" + } + ], + "minimum-stability": "dev", + "stability-flags": { + "phpcompatibility/php-compatibility": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": [], diff --git a/tests/RulesetTest.php b/tests/RulesetTest.php new file mode 100644 index 0000000..9f33eed --- /dev/null +++ b/tests/RulesetTest.php @@ -0,0 +1,370 @@ + '10up-Default', + ]; + + /** + * String returned by PHP_CodeSniffer report for an Error. + */ + const ERROR_TYPE = 'ERROR'; + + /** + * Init the object by processing the test file. + * + * @param string $ruleset Name of the ruleset e.g. TenUpDefault. + * @param array $expected The array of expected errors, warnings and messages. + */ + public function __construct( $ruleset, $expected = [] ) { + $this->ruleset = $ruleset; + $this->expected = $expected; + + if ( isset( $this->directory_mapping[ $ruleset ] ) ) { + $this->ruleset_directory = $this->directory_mapping[ $ruleset ]; + } else { + $this->ruleset_directory = $ruleset; + } + + // Travis and Windows support. + $phpcs_bin = getenv( 'PHPCS_BIN' ); + if ( $phpcs_bin === false ) { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv -- This is test code, not production. + putenv( 'PHPCS_BIN=phpcs' ); + } else { + $this->phpcs_bin = realpath( $phpcs_bin ); + } + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + printf( 'Testing the ' . $this->ruleset . ' ruleset.' . PHP_EOL ); + + $output = $this->collect_phpcs_result(); + + if ( empty( $output ) || ! is_object( $output ) ) { + printf( 'The PHPCS command checking the ruleset hasn\'t returned any issues. Bailing ...' . PHP_EOL ); + exit( 1 ); // Die early, if we don't have any output. + } + + $this->process_output( $output ); + } + + /** + * Run all the tests and return whether test was successful. + * + * @return bool + */ + public function passes() { + $this->run(); + + return ! $this->found_issue; + } + + /** + * Run all the tests. + * + * @return void + */ + private function run() { + // Check for missing expected values. + $this->check_missing_expected_values(); + // Check for extra values which were not expected. + $this->check_unexpected_values(); + // Check for expected messages. + $this->check_messages(); + } + + /** + * Collect the PHP_CodeSniffer result. + * + * @return array Returns an associative array with keys of `totals` and `files`. + */ + private function collect_phpcs_result() { + $php = ''; + if ( \PHP_BINARY && in_array( \PHP_SAPI, [ 'cgi-fcgi', 'cli', 'cli-server', 'phpdbg' ], true ) ) { + $php = \PHP_BINARY . ' '; + } + + $shell = sprintf( + '%1$s%2$s --severity=1 --standard=%3$s --report=json ./%3$s/ruleset-test.inc', + $php, // Current PHP executable if available. + $this->phpcs_bin, + $this->ruleset_directory + ); + + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec -- This is test code, not production. + $output = shell_exec( $shell ); + + return json_decode( $output ); + } + + /** + * Process the Decoded JSON output from PHP_CodeSniffer. + * + * @param \stdClass $output Decoded JSON output from PHP_CodeSniffer. + * + * @return void + */ + private function process_output( $output ) { + foreach ( $output->files as $file ) { + $this->process_file( $file ); + } + } + + /** + * Process single file of within PHP_CodeSniffer results. + * + * @param \stdClass $file File output. + * + * @return void + */ + private function process_file( $file ) { + foreach ( $file->messages as $violation ) { + $this->process_violation( $violation ); + } + } + + /** + * Process single violation within PHP_CodeSniffer results. + * + * @param \stdClass $violation Violation data. + * + * @return void + */ + private function process_violation( $violation ) { + if ( $this->violation_type_is_error( $violation ) ) { + $this->add_error_for_line( $violation->line ); + } else { + $this->add_warning_for_line( $violation->line ); + } + + $this->add_message_for_line( $violation->line, $violation->message ); + } + + /** + * Check if violation is an error. + * + * @param \stdClass $violation Violation data. + * + * @return bool True if string matches error type. + */ + private function violation_type_is_error( $violation ) { + return $violation->type === self::ERROR_TYPE; + } + + /** + * Add 1 to the number of errors for the given line. + * + * @param int $line Line number. + * + * @return void + */ + private function add_error_for_line( $line ) { + $this->errors[ $line ] = isset( $this->errors[ $line ] ) ? ++ $this->errors[ $line ] : 1; + } + + /** + * Add 1 to the number of errors for the given line. + * + * @param int $line Line number. + * + * @return void + */ + private function add_warning_for_line( $line ) { + $this->warnings[ $line ] = isset( $this->warnings[ $line ] ) ? ++ $this->warnings[ $line ] : 1; + } + + /** + * Add message for the given line. + * + * @param int $line Line number. + * @param string $message Message. + * + * @return void + */ + private function add_message_for_line( $line, $message ) { + $this->messages[ $line ] = ( ! isset( $this->messages[ $line ] ) || ! is_array( $this->messages[ $line ] ) ) ? [ $message ] : array_merge( $this->messages[ $line ], + [ $message ] ); + } + + /** + * Check whether all expected numbers of errors and warnings are present in the output. + * + * @return void + */ + private function check_missing_expected_values() { + foreach ( $this->expected as $type => $lines ) { + if ( $type === 'messages' ) { + continue; + } + + foreach ( $lines as $line_number => $expected_count_of_type_violations ) { + if ( $expected_count_of_type_violations === 0 ) { + continue; + } + + if ( ! isset( $this->{$type}[ $line_number ] ) ) { + $this->error_warning_message( $expected_count_of_type_violations, $type, 0, $line_number ); + } elseif ( $this->{$type}[ $line_number ] !== $expected_count_of_type_violations ) { + $this->error_warning_message( $expected_count_of_type_violations, $type, + $this->{$type}[ $line_number ], $line_number ); + } + + unset( $this->{$type}[ $line_number ] ); + } + } + } + + /** + * Check whether there are no unexpected numbers of errors and warnings. + * + * @return void + */ + private function check_unexpected_values() { + foreach ( [ 'errors', 'warnings' ] as $type ) { + foreach ( $this->$type as $line_number => $actual_count_of_type_violations ) { + if ( $actual_count_of_type_violations === 0 ) { + continue; + } + + if ( ! isset( $this->expected[ $type ][ $line_number ] ) ) { + $this->error_warning_message( 0, $type, $actual_count_of_type_violations, $line_number ); + } elseif ( $actual_count_of_type_violations !== $this->expected[ $type ][ $line_number ] ) { + $this->error_warning_message( $this->expected[ $type ][ $line_number ], $type, + $actual_count_of_type_violations, $line_number ); + } + } + } + } + + /** + * Check whether all expected messages are present and whether there are no unexpected messages. + * + * @return void + */ + private function check_messages() { + foreach ( $this->expected['messages'] as $line_number => $messages ) { + foreach ( $messages as $message ) { + if ( ! isset( $this->messages[ $line_number ] ) ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + printf( 'Expected "%s" but found no message for line %d' . PHP_EOL, $message, $line_number ); + $this->found_issue = true; + } elseif ( ! in_array( $message, $this->messages[ $line_number ], true ) ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + printf( 'Expected message "%s" was not found for line %d.' . PHP_EOL, $message, $line_number ); + $this->found_issue = true; + } + } + } + foreach ( $this->messages as $line_number => $messages ) { + foreach ( $messages as $message ) { + if ( isset( $this->expected['messages'][ $line_number ] ) ) { + if ( ! in_array( $message, $this->expected['messages'][ $line_number ], true ) ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + printf( 'Unexpected message "%s" was found for line %d.' . PHP_EOL, $message, $line_number ); + $this->found_issue = true; + } + } + } + } + } + + /** + * Print out the message reporting found issues. + * + * @param int $expected Expected number of issues. + * @param string $type The type of the issue. + * @param int $number Real number of issues. + * @param int $line Line number. + * + * @return void + */ + private function error_warning_message( $expected, $type, $number, $line ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + printf( 'Expected %d %s, found %d on line %d.' . PHP_EOL, $expected, $type, $number, $line ); + $this->found_issue = true; + } +}