From 0e2e7b57e0044f74f3a61c7981d807175d2b34ad Mon Sep 17 00:00:00 2001 From: Rodrigo Primo Date: Wed, 21 May 2025 16:48:08 -0300 Subject: [PATCH 1/2] Refactor FeatureComplete code for color support and formatting help text This commit moves the code related to checking color and formatting the help text to a new helper class. This way it can be reused when the new DocCodeExamples script is introduced. Tests were added for the new HelpTextFormatter::format() method. --- Scripts/FeatureComplete/Config.php | 108 +------------------- Scripts/Utils/HelpTextFormatter.php | 137 ++++++++++++++++++++++++++ Tests/Utils/HelpTextFormatterTest.php | 114 +++++++++++++++++++++ phpunit.xml.dist | 1 + phpunitlte9.xml.dist | 1 + 5 files changed, 257 insertions(+), 104 deletions(-) create mode 100644 Scripts/Utils/HelpTextFormatter.php create mode 100644 Tests/Utils/HelpTextFormatterTest.php diff --git a/Scripts/FeatureComplete/Config.php b/Scripts/FeatureComplete/Config.php index 3ee24e2..3d64677 100644 --- a/Scripts/FeatureComplete/Config.php +++ b/Scripts/FeatureComplete/Config.php @@ -10,6 +10,7 @@ namespace PHPCSDevTools\Scripts\FeatureComplete; +use PHPCSDevTools\Scripts\Utils\HelpTextFormatter; use PHPCSDevTools\Scripts\Utils\Writer; use RuntimeException; @@ -35,20 +36,6 @@ final class Config { - /** - * Max width for help text. - * - * @var int - */ - const MAX_WIDTH = 80; - - /** - * Margin for help options. - * - * @var string - */ - const LEFT_MARGIN = ' '; - /** * Writer for sending output. * @@ -284,7 +271,7 @@ protected function processCliCommand() if (empty($args)) { // No options set. - $this->showColored = $this->isColorSupported(); + $this->showColored = HelpTextFormatter::isColorSupported(); return; } @@ -296,7 +283,7 @@ protected function processCliCommand() } elseif (isset($argsFlipped['--colors'])) { $this->showColored = true; } else { - $this->showColored = $this->isColorSupported(); + $this->showColored = HelpTextFormatter::isColorSupported(); } if (isset($argsFlipped['-h']) @@ -373,41 +360,6 @@ static function ($subdir) { } } - /** - * Detect whether or not the CLI supports colored output. - * - * @codeCoverageIgnore - * - * @return bool - */ - protected function isColorSupported() - { - // Windows. - if (\DIRECTORY_SEPARATOR === '\\') { - if (\getenv('ANSICON') !== false || \getenv('ConEmuANSI') === 'ON') { - return true; - } - - if (\function_exists('sapi_windows_vt100_support')) { - // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.sapi_windows_vt100_supportFound - return @\sapi_windows_vt100_support(\STDOUT); - } - - return false; - } - - if (\getenv('GITHUB_ACTIONS')) { - return true; - } - - // Linux/MacOS. - if (\function_exists('posix_isatty')) { - return @\posix_isatty(\STDOUT); - } - - return false; - } - /** * Retrieve the version number of this script. * @@ -429,58 +381,6 @@ public function getVersion() */ private function getHelp() { - $output = ''; - foreach ($this->helpTexts as $section => $options) { - $longestOptionLength = 0; - foreach ($options as $option) { - if (isset($option['arg'])) { - $longestOptionLength = \max($longestOptionLength, \strlen($option['arg'])); - } - } - - if ($this->showColored === true) { - $output .= "\033[33m{$section}:\033[0m" . \PHP_EOL; - } else { - $output .= "{$section}:" . \PHP_EOL; - } - - $descWidth = (self::MAX_WIDTH - ($longestOptionLength + 1 + \strlen(self::LEFT_MARGIN))); - $descBreak = \PHP_EOL . self::LEFT_MARGIN . \str_pad(' ', ($longestOptionLength + 1)); - - foreach ($options as $option) { - if (isset($option['text'])) { - $text = $option['text']; - if ($this->showColored === true) { - $text = \preg_replace('`(\[[^\]]+\])`', "\033[36m" . '$1' . "\033[0m", $text); - } - $output .= self::LEFT_MARGIN . $text . \PHP_EOL; - } - - if (isset($option['arg'])) { - $arg = \str_pad($option['arg'], $longestOptionLength); - if ($this->showColored === true) { - $arg = \preg_replace('`(<[^>]+>)`', "\033[0m\033[36m" . '$1', $arg); - $arg = "\033[32m{$arg}\033[0m"; - } - - $descText = \wordwrap($option['desc'], $descWidth, $descBreak); - $desc = \explode('. ', $option['desc']); - if (\count($desc) > 1) { - $descText = ''; - foreach ($desc as $key => $sentence) { - $descText .= ($key === 0) ? '' : $descBreak; - $descText .= \wordwrap($sentence, $descWidth, $descBreak); - $descText = \rtrim($descText, '.') . '.'; - } - } - - $output .= self::LEFT_MARGIN . $arg . ' ' . $descText . \PHP_EOL; - } - } - - $output .= \PHP_EOL; - } - - return $output; + return HelpTextFormatter::format($this->helpTexts, $this->showColored); } } diff --git a/Scripts/Utils/HelpTextFormatter.php b/Scripts/Utils/HelpTextFormatter.php new file mode 100644 index 0000000..2be4a18 --- /dev/null +++ b/Scripts/Utils/HelpTextFormatter.php @@ -0,0 +1,137 @@ +>> $helpTexts The help texts to format. + * @param bool $showColored Whether to use colored output. + * + * @return string The formatted help text. + */ + public static function format(array $helpTexts, $showColored) + { + $output = ''; + foreach ($helpTexts as $section => $options) { + $longestOptionLength = 0; + foreach ($options as $option) { + if (isset($option['arg'])) { + $longestOptionLength = \max($longestOptionLength, \strlen($option['arg'])); + } + } + + if ($showColored === true) { + $output .= "\033[33m{$section}:\033[0m" . \PHP_EOL; + } else { + $output .= "{$section}:" . \PHP_EOL; + } + + $descWidth = (self::MAX_WIDTH - ($longestOptionLength + 1 + \strlen(self::LEFT_MARGIN))); + $descBreak = \PHP_EOL . self::LEFT_MARGIN . \str_pad(' ', ($longestOptionLength + 1)); + + foreach ($options as $option) { + if (isset($option['text'])) { + $text = $option['text']; + if ($showColored === true) { + $text = \preg_replace('`(\[[^\]]+\])`', "\033[36m" . '$1' . "\033[0m", $text); + } + $output .= self::LEFT_MARGIN . $text . \PHP_EOL; + } + + if (isset($option['arg'])) { + $arg = \str_pad($option['arg'], $longestOptionLength); + if ($showColored === true) { + $arg = \preg_replace('`(<[^>]+>)`', "\033[0m\033[36m" . '$1', $arg); + $arg = "\033[32m{$arg}\033[0m"; + } + + $descText = \wordwrap($option['desc'], $descWidth, $descBreak); + $desc = \explode('. ', $option['desc']); + if (\count($desc) > 1) { + $descText = ''; + foreach ($desc as $key => $sentence) { + $descText .= ($key === 0) ? '' : $descBreak; + $descText .= \wordwrap($sentence, $descWidth, $descBreak); + $descText = \rtrim($descText, '.') . '.'; + } + } + + $output .= self::LEFT_MARGIN . $arg . ' ' . $descText . \PHP_EOL; + } + } + + $output .= \PHP_EOL; + } + + return $output; + } + + /** + * Detect whether or not the CLI supports colored output. + * + * @codeCoverageIgnore + * + * @return bool + */ + public static function isColorSupported() + { + // Windows. + if (\DIRECTORY_SEPARATOR === '\\') { + if (\getenv('ANSICON') !== false || \getenv('ConEmuANSI') === 'ON') { + return true; + } + + if (\function_exists('sapi_windows_vt100_support')) { + // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.sapi_windows_vt100_supportFound + return @\sapi_windows_vt100_support(\STDOUT); + } + + return false; + } + + if (\getenv('GITHUB_ACTIONS')) { + return true; + } + + // Linux/MacOS. + if (\function_exists('posix_isatty')) { + return @\posix_isatty(\STDOUT); + } + + return false; + } +} diff --git a/Tests/Utils/HelpTextFormatterTest.php b/Tests/Utils/HelpTextFormatterTest.php new file mode 100644 index 0000000..195a3c3 --- /dev/null +++ b/Tests/Utils/HelpTextFormatterTest.php @@ -0,0 +1,114 @@ +>> $helpTexts The help texts to format. + * @param string $expected The expected formatted output. + * @param bool $useColor Whether to use colored output. + * + * @return void + */ + public function testFormat(array $helpTexts, $expected, $useColor) + { + $this->assertSame($expected, HelpTextFormatter::format($helpTexts, $useColor)); + } + + /** + * Data provider for testing the format() method. + * + * @return array>>|string|bool>> + */ + public static function dataFormat() + { + return [ + 'empty help texts' => [ + 'helpTexts' => [], + 'expected' => '', + 'useColor' => false, + ], + 'empty section' => [ + 'helpTexts' => [ + 'Empty Section' => [], + ], + 'expected' => 'Empty Section:' . PHP_EOL . PHP_EOL, + 'useColor' => false, + ], + 'without color' => [ + 'helpTexts' => [ + 'Usage' => [ + [ + 'text' => 'Command [options]', + ], + ], + 'Options' => [ + [ + 'arg' => '--help', + 'desc' => 'Display this help message.', + ], + [ + 'arg' => '--version', + 'desc' => 'Display version information.', + ], + [ + 'arg' => '', + 'desc' => 'The file to process. This is a test with multiple sentences. Each sentence should be properly wrapped.', + ], + ], + ], + 'expected' => 'Usage:' . PHP_EOL . + ' Command [options]' . PHP_EOL . PHP_EOL . + 'Options:' . PHP_EOL . + ' --help Display this help message.' . PHP_EOL . + ' --version Display version information.' . PHP_EOL . + ' The file to process.' . PHP_EOL . + ' This is a test with multiple sentences.' . PHP_EOL . + ' Each sentence should be properly wrapped.' . PHP_EOL . PHP_EOL, + 'useColor' => false, + ], + 'with color' => [ + 'helpTexts' => [ + 'Usage' => [ + [ + 'text' => 'Command [options]', + ], + ], + 'Options' => [ + [ + 'arg' => '--help', + 'desc' => 'Display this help message.', + ], + ], + ], + 'expected' => "\033[33mUsage:\033[0m" . PHP_EOL . + " Command \033[36m[options]\033[0m" . PHP_EOL . PHP_EOL . + "\033[33mOptions:\033[0m" . PHP_EOL . + " \033[32m--help\033[0m Display this help message." . PHP_EOL . PHP_EOL, + 'useColor' => true, + ], + ]; + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1e48290..ed1e1a0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -28,6 +28,7 @@ ./Tests/DocsXsd/ ./Tests/FeatureComplete/ + ./Tests/Utils/ diff --git a/phpunitlte9.xml.dist b/phpunitlte9.xml.dist index 047630d..7a37473 100644 --- a/phpunitlte9.xml.dist +++ b/phpunitlte9.xml.dist @@ -19,6 +19,7 @@ ./Tests/DocsXsd/ ./Tests/FeatureComplete/ + ./Tests/Utils/ From 8e1daa514e1efbae95984b1f73548d2588036e4a Mon Sep 17 00:00:00 2001 From: Rodrigo Primo Date: Tue, 9 Apr 2024 15:54:36 -0300 Subject: [PATCH 2/2] Add script to validate code examples in sniff documentation This commit introduces a new script that verifies code examples in PHPCS sniff documentation. The script checks that "Valid" examples don't trigger the sniff and "Invalid" examples do trigger the sniff, ensuring code examples correctly demonstrate the behavior of the sniff. Noteworthy implementation decisions: - The script will be available in the Composer vendor/bin directory as `phpcs-check-doc-examples`. - To check for syntax errors in the code examples, the code uses `token_get_all()` with the flag TOKEN_PARSE. This means that PHP >= 7.0 is required to run this script while the rest of the repository requires PHP >= 5.4. A requirement check was added to `phpcs-check-doc-examples` to alert users if they are running PHP < 7.0, and a warning was added to the top of the file informing that `phpcs-check-doc-examples` must remain compatible with PHP 5.4 for the requirement check to work. - The script is unable to differentiate between multiple code examples in the same block. This means that for invalid examples, the script might miss ones that don't trigger the sniff if, in the same block, there is at least one example that does trigger the sniff. - The tests for this script run on a separate GitHub action job as they need to be tested against a different set of PHP versions than the rest of the tests in this repository, and they need to be tested against multiple PHPCS versions and OSes. The DebugSniff tests need to be tested against multiple PHPCS versions but not multiple OSes, while the tests for the other tools need to be tested against multiple OSes but not multiple PHPCS versions. How to use the script: - Run it from the root directory of a PHPCS standard: ``` $ /path/to/script/phpcs-check-doc-examples ``` The script will search for all sniff documentation files and check their code examples. For other ways to run the script check the help page: ``` $ /path/to/script/phpcs-check-doc-examples --help ``` --- .github/workflows/quicktest.yml | 91 +++- .github/workflows/test.yml | 199 ++++++- README.md | 44 ++ Scripts/DocCodeExamples/Check.php | 208 ++++++++ Scripts/DocCodeExamples/CodeBlock.php | 54 ++ .../DocCodeExamples/CodeBlocksExtractor.php | 126 +++++ Scripts/DocCodeExamples/Config.php | 326 +++++++++++ Scripts/DocCodeExamples/Functions.php | 27 + Scripts/DocCodeExamples/Helper.php | 38 ++ Scripts/DocCodeExamples/XmlDocValidator.php | 348 ++++++++++++ Tests/DocCodeExamples/CheckTest.php | 504 ++++++++++++++++++ .../CodeBlocksExtractorTest.php | 164 ++++++ Tests/DocCodeExamples/ConfigTest.php | 487 +++++++++++++++++ Tests/DocCodeExamples/EndToEndTest.php | 186 +++++++ Tests/DocCodeExamples/HelperTest.php | 65 +++ .../ManipulateObjectsTrait.php | 57 ++ Tests/DocCodeExamples/PHPCSConfigLoader.php | 40 ++ Tests/DocCodeExamples/VersionTestUtils.php | 21 + Tests/DocCodeExamples/XmlDocValidatorTest.php | 236 ++++++++ ...TestXmlDocValidatorConstructorStandard.xml | 0 .../Docs/Examples/CorrectExamplesStandard.xml | 38 ++ .../IncorrectInvalidExampleStandard.xml | 20 + .../IncorrectValidExampleStandard.xml | 20 + .../Docs/Examples/PhpOpenTagStandard.xml | 55 ++ .../Docs/Examples/PhpcsUtilsCacheStandard.xml | 26 + .../Examples/SyntaxErrorExampleStandard.xml | 19 + .../Sniffs/Examples/BaseExamplesSniff.php | 28 + .../Sniffs/Examples/CorrectExamplesSniff.php | 11 + .../Examples/IncorrectInvalidExampleSniff.php | 11 + .../Examples/IncorrectValidExampleSniff.php | 11 + .../Sniffs/Examples/PhpOpenTagSniff.php | 11 + .../Sniffs/Examples/PhpcsUtilsCacheSniff.php | 29 + .../Examples/SyntaxErrorExampleSniff.php | 11 + .../CheckCodeExamplesStandard/ruleset.xml | 4 + .../Category/MissingDocsDirStandard.xml | 0 .../Docs/Category/SniffName.txt | 1 + .../Docs/MissingCategoryDirStandard.xml | 0 .../CodeBlocksExtractor/EmptyFile.xml | 0 .../CodeBlocksExtractor/InvalidXMLFile.xml | 1 + .../CodeBlocksExtractor/NoCodeBlocks.xml | 8 + .../CodeBlocksExtractor/OneValidBlock.xml | 14 + .../ValidAndInvalidBlocks.xml | 36 ++ .../InvalidPathToDocStandard.xml | 7 + ...rrorWhenStandardIsNotInstalledStandard.xml | 7 + bin/phpcs-check-doc-examples | 87 +++ composer.json | 12 +- phpcs.xml.dist | 86 +++ phpunit.xml.dist | 3 + phpunitlte9.xml.dist | 3 + 49 files changed, 3774 insertions(+), 6 deletions(-) create mode 100644 Scripts/DocCodeExamples/Check.php create mode 100644 Scripts/DocCodeExamples/CodeBlock.php create mode 100644 Scripts/DocCodeExamples/CodeBlocksExtractor.php create mode 100644 Scripts/DocCodeExamples/Config.php create mode 100644 Scripts/DocCodeExamples/Functions.php create mode 100644 Scripts/DocCodeExamples/Helper.php create mode 100644 Scripts/DocCodeExamples/XmlDocValidator.php create mode 100644 Tests/DocCodeExamples/CheckTest.php create mode 100644 Tests/DocCodeExamples/CodeBlocksExtractorTest.php create mode 100644 Tests/DocCodeExamples/ConfigTest.php create mode 100644 Tests/DocCodeExamples/EndToEndTest.php create mode 100644 Tests/DocCodeExamples/HelperTest.php create mode 100644 Tests/DocCodeExamples/ManipulateObjectsTrait.php create mode 100644 Tests/DocCodeExamples/PHPCSConfigLoader.php create mode 100644 Tests/DocCodeExamples/VersionTestUtils.php create mode 100644 Tests/DocCodeExamples/XmlDocValidatorTest.php create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Constructor/TestXmlDocValidatorConstructorStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpOpenTagStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpcsUtilsCacheStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/BaseExamplesSniff.php create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/CorrectExamplesSniff.php create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/IncorrectInvalidExampleSniff.php create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/IncorrectValidExampleSniff.php create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/PhpOpenTagSniff.php create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/PhpcsUtilsCacheSniff.php create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/SyntaxErrorExampleSniff.php create mode 100644 Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/ruleset.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Category/MissingDocsDirStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Docs/Category/SniffName.txt create mode 100644 Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Docs/MissingCategoryDirStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/EmptyFile.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/InvalidXMLFile.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/NoCodeBlocks.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/OneValidBlock.xml create mode 100644 Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/ValidAndInvalidBlocks.xml create mode 100644 Tests/Fixtures/DocCodeExamples/InvalidPathToDocStandard.xml create mode 100644 Tests/Fixtures/DocCodeExamples/UninstalledStandard/Docs/Category/TestErrorWhenStandardIsNotInstalledStandard.xml create mode 100644 bin/phpcs-check-doc-examples diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 298d8cf..1b55ccc 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -68,10 +68,14 @@ jobs: if: ${{ matrix.php >= '7.2' }} run: composer lint - - name: Lint against parse errors (PHP < 7.2) - if: ${{ matrix.php < '7.2' }} + - name: Lint against parse errors (PHP < 7.2 && PHP >= 7.0) + if: ${{ matrix.php < '7.2' && matrix.php >= '7.0' }} run: composer lintlt72 + - name: Lint against parse errors (PHP < 7.0) + if: ${{ matrix.php < '7.0' }} + run: composer lintlt70 + # Check that any sniffs available are feature complete. # This acts as an integration test for the feature completeness script, # which is why it is run against various PHP versions. @@ -182,3 +186,86 @@ jobs: - name: Run the unit tests for the PHPCSDebug sniff run: composer test-sniff${{ steps.phpunit_script.outputs.SUFFIX }} + + # Run CI checks/tests for the DocCodeExamples script as it requires PHP >= 7.0. + quicktest-doc-code-examples: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: ['ubuntu-latest', 'windows-latest'] + php: ['7.0', 'latest'] + phpcs_version: ['dev-master'] + + include: + - os: 'ubuntu-latest' + php: '7.0' + phpcs_version: '3.1.0' + - os: 'windows-latest' + php: '7.0' + phpcs_version: '3.1.0' + + name: "QTest: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }} (${{ matrix.os == 'windows-latest' && 'Win' || 'Linux' }})" + + steps: + - name: Prepare git to leave line endings alone + run: git config --global core.autocrlf input + + - name: Checkout code + uses: actions/checkout@v4 + + # On stable PHPCS versions, 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 + shell: bash + run: | + if [ "${{ matrix.phpcs_version }}" != "dev-master" ]; then + echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> "$GITHUB_OUTPUT" + else + echo 'PHP_INI=error_reporting=-1, display_errors=On' >> "$GITHUB_OUTPUT" + fi + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: ${{ steps.set_ini.outputs.PHP_INI }} + coverage: none + + - name: 'Composer: adjust dependencies' + run: | + # Set the PHPCS version to be used in the tests. + composer require --no-update squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-scripts --no-interaction + # Remove the PHPCSDevCS dependency as it has different PHPCS requirements and would block installs. + composer remove --no-update --dev phpcsstandards/phpcsdevcs --no-scripts --no-interaction + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Grab PHPUnit version + id: phpunit_version + shell: bash + # yamllint disable rule:line-length + run: echo "VERSION=$(vendor/bin/phpunit --version | grep --only-matching --max-count=1 --extended-regexp '\b[0-9]+\.[0-9]+')" >> "$GITHUB_OUTPUT" + # yamllint enable rule:line-length + + - name: Determine PHPUnit composer script to use + id: phpunit_script + shell: bash + run: | + if [ "${{ startsWith( steps.phpunit_version.outputs.VERSION, '11.' ) }}" == "true" ]; then + echo 'SUFFIX=' >> "$GITHUB_OUTPUT" + elif [ "${{ startsWith( steps.phpunit_version.outputs.VERSION, '10.' ) }}" == "true" ]; then + echo 'SUFFIX=' >> "$GITHUB_OUTPUT" + else + echo 'SUFFIX=-lte9' >> "$GITHUB_OUTPUT" + fi + + - name: Run the unit tests for the DocCodeExamples script + run: composer test-doc-code-examples${{ steps.phpunit_script.outputs.SUFFIX }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c35b256..969e183 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,9 +73,13 @@ jobs: if: ${{ matrix.php >= '7.2' }} run: composer lint -- --checkstyle | cs2pr - - name: Lint against parse errors (PHP < 7.2) - if: ${{ matrix.php < '7.2' }} - run: composer lintlt72 -- --checkstyle | cs2pr + - name: Lint against parse errors (PHP < 7.2 && PHP >= 7.0) + if: ${{ matrix.php < '7.2' && matrix.php >= '7.0' }} + run: composer lintlt72 + + - name: Lint against parse errors (PHP < 7.0) + if: ${{ matrix.php < '7.0' }} + run: composer lintlt70 # Check that any sniffs available are feature complete. # This also acts as an integration test for the feature completeness script, @@ -229,3 +233,192 @@ jobs: - name: Run the unit tests for the PHPCSDebug sniff run: composer test-sniff${{ steps.phpunit_script.outputs.SUFFIX }} + + # Run CI checks/tests for the DocCodeExamples script as it requires PHP >= 7.0. + test-doc-code-examples: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: ['ubuntu-latest', 'windows-latest'] + # IMPORTANT: test runs shouldn't fail because of PHPCS being incompatible with a PHP version. + # - PHPCS will run without errors on PHP 7.0 - 7.2 on any version. + # - PHP 7.3 needs PHPCS 3.3.1+ to run without errors. + # - PHP 7.4 needs PHPCS 3.5.0+ to run without errors. + # - PHP 8.0 needs PHPCS 3.5.7+ to run without errors. + # - PHP 8.1 needs PHPCS 3.6.1+ to run without errors. + # - PHP 8.2 needs PHPCS 3.6.1+ to run without errors. + # - PHP 8.3 needs PHPCS 3.8.0+ to run without errors (though the errors don't affect this package). + # - PHP 8.4 needs PHPCS 3.8.0+ to run without errors (officially 3.11.0, but 3.8.0 will work fine). + php: ['7.0', '7.1', '7.2'] + phpcs_version: ['3.1.0', 'dev-master'] + + include: + # Complete the matrix, while preventing issues with PHPCS versions incompatible with certain PHP versions. + - os: 'ubuntu-latest' + php: '7.3' + phpcs_version: 'dev-master' + - os: 'windows-latest' + php: '7.3' + phpcs_version: 'dev-master' + - os: 'ubuntu-latest' + php: '7.3' + phpcs_version: '3.3.1' + - os: 'windows-latest' + php: '7.3' + phpcs_version: '3.3.1' + + - os: 'ubuntu-latest' + php: '7.4' + phpcs_version: 'dev-master' + - os: 'windows-latest' + php: '7.4' + phpcs_version: 'dev-master' + - os: 'ubuntu-latest' + php: '7.4' + phpcs_version: '3.5.0' + - os: 'windows-latest' + php: '7.4' + phpcs_version: '3.5.0' + + - os: 'ubuntu-latest' + php: '8.0' + phpcs_version: 'dev-master' + - os: 'windows-latest' + php: '8.0' + phpcs_version: 'dev-master' + - os: 'ubuntu-latest' + php: '8.0' + phpcs_version: '3.5.7' + - os: 'windows-latest' + php: '8.0' + phpcs_version: '3.5.7' + + - os: 'ubuntu-latest' + php: '8.1' + phpcs_version: 'dev-master' + - os: 'windows-latest' + php: '8.1' + phpcs_version: 'dev-master' + - os: 'ubuntu-latest' + php: '8.1' + phpcs_version: '3.6.1' + - os: 'windows-latest' + php: '8.1' + phpcs_version: '3.6.1' + + - os: 'ubuntu-latest' + php: '8.2' + phpcs_version: 'dev-master' + - os: 'windows-latest' + php: '8.2' + phpcs_version: 'dev-master' + - os: 'ubuntu-latest' + php: '8.2' + phpcs_version: '3.6.1' + - os: 'windows-latest' + php: '8.2' + phpcs_version: '3.6.1' + + - os: 'ubuntu-latest' + php: '8.3' + phpcs_version: 'dev-master' + - os: 'windows-latest' + php: '8.3' + phpcs_version: 'dev-master' + - os: 'ubuntu-latest' + php: '8.3' + phpcs_version: '3.8.0' + - os: 'windows-latest' + php: '8.3' + phpcs_version: '3.8.0' + + - os: 'ubuntu-latest' + php: '8.4' + phpcs_version: 'dev-master' + - os: 'windows-latest' + php: '8.4' + phpcs_version: 'dev-master' + - os: 'ubuntu-latest' + php: '8.4' + phpcs_version: '3.8.0' + - os: 'windows-latest' + php: '8.4' + phpcs_version: '3.8.0' + + # Experimental builds. These are allowed to fail. + - os: 'ubuntu-latest' + php: '7.4' + phpcs_version: '4.x-dev' + + - os: 'ubuntu-latest' + php: '8.5' # Nightly. + phpcs_version: 'dev-master' + + name: "Test: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }} (${{ matrix.os == 'windows-latest' && 'Win' || 'Linux' }})" + + continue-on-error: ${{ matrix.php == '8.5' || matrix.phpcs_version == '4.x-dev' }} + + steps: + - name: Prepare git to leave line endings alone + run: git config --global core.autocrlf input + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup ini config + id: set_ini + shell: bash + run: | + # On stable PHPCS versions, 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. + if [[ "${{ matrix.phpcs_version }}" != "dev-master" && "${{ matrix.phpcs_version }}" != "4.x-dev" ]]; then + echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> "$GITHUB_OUTPUT" + else + echo 'PHP_INI=error_reporting=-1, display_errors=On' >> "$GITHUB_OUTPUT" + fi + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: ${{ steps.set_ini.outputs.PHP_INI }} + coverage: none + + - name: 'Composer: adjust dependencies' + run: | + # Set the PHPCS version to be used in the tests. + composer require --no-update squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-scripts --no-interaction + # Remove the PHPCSDevCS dependency as it has different PHPCS requirements and would block installs. + composer remove --no-update --dev phpcsstandards/phpcsdevcs --no-scripts --no-interaction + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + with: + composer-options: ${{ matrix.php == '8.5' && '--ignore-platform-req=php+' || '' }} + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Grab PHPUnit version + id: phpunit_version + shell: bash + # yamllint disable rule:line-length + run: echo "VERSION=$(vendor/bin/phpunit --version | grep --only-matching --max-count=1 --extended-regexp '\b[0-9]+\.[0-9]+')" >> "$GITHUB_OUTPUT" + # yamllint enable rule:line-length + + - name: Determine PHPUnit composer script to use + id: phpunit_script + shell: bash + run: | + if [ "${{ startsWith( steps.phpunit_version.outputs.VERSION, '11.' ) }}" == "true" ]; then + echo 'SUFFIX=' >> "$GITHUB_OUTPUT" + elif [ "${{ startsWith( steps.phpunit_version.outputs.VERSION, '10.' ) }}" == "true" ]; then + echo 'SUFFIX=' >> "$GITHUB_OUTPUT" + else + echo 'SUFFIX=-lte9' >> "$GITHUB_OUTPUT" + fi + + - name: Run the unit tests for the DocCodeExamples script + run: composer test-doc-code-examples${{ steps.phpunit_script.outputs.SUFFIX }} diff --git a/README.md b/README.md index 0249b62..a2c8d01 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ This is a set of tools to assist developers of sniffs for [PHP CodeSniffer][phpc + [Stand-alone Installation](#stand-alone-installation) * [Features](#features) + [Checking whether all sniffs in a PHPCS standard are feature complete](#checking-whether-all-sniffs-in-a-phpcs-standard-are-feature-complete) + + [Checking code examples in sniff documentation are correct](#checking-code-examples-in-sniff-documentation-are-correct) + [Sniff Debugging](#sniff-debugging) + [Documentation XSD Validation](#documentation-xsd-validation) * [Contributing](#contributing) @@ -133,6 +134,49 @@ directories One or more specific directories to examine. -V, --version Display the current version of this script. ``` +### Checking code examples in sniff documentation are correct + +You can verify that the code examples in your sniff documentation XML files correctly demonstrate the behavior of the sniffs. In other words, check that "Valid" examples don't trigger the sniff and "Invalid" examples do trigger the sniff. + +Note that while this repository requires PHP >= 5.4, this script requires PHP >= 7.0. + +Be aware that this script is unable to differentiate between multiple code examples in the same `` block. This means that for invalid examples, the script might miss ones that don't trigger the sniff if, in the same `` block, there is at least one example that does trigger the sniff. + +To use the tool, run it from the root of your standards repo like so: +```bash +# When installed as a project dependency: +vendor/bin/phpcs-check-doc-examples + +# When installed globally with Composer: +phpcs-check-doc-examples + +# When installed as a git clone or otherwise: +php path/to/PHPCSDevTools/bin/phpcs-check-doc-examples +``` + +If all is good, the script will exit with code 0 and a summary of what was checked. + +If there are issues with the code examples, you will see error messages for each problematic example, like so: +```text +Errors found while processing path/to/project/StandardName/Docs/Category/SniffNameStandard.xml: + +ERROR: Code block is valid and PHPCS should have returned nothing, but instead it returned an error. +Code block title: "Valid: invalid valid code example." +Code block content: "function sniffValidationWillFail() {}" +``` + +#### Options +```text +directories|files One or more specific directories or files to examine. + Defaults to the directory from which the script is run. +--exclude= Comma-delimited list of relative paths of directories to + exclude from the scan. +--ignore-sniffs= Comma-delimited list of sniffs to ignore. +--colors Enable colors in console output. (disables auto detection of color support) +--no-colors Disable colors in console output. +--help Print the script help page. +-V, --version Display the current version of this script. +``` ### Sniff Debugging diff --git a/Scripts/DocCodeExamples/Check.php b/Scripts/DocCodeExamples/Check.php new file mode 100644 index 0000000..e07c072 --- /dev/null +++ b/Scripts/DocCodeExamples/Check.php @@ -0,0 +1,208 @@ + + */ + protected $xmlFiles = []; + + /** + * Constructor. + * + * @param \PHPCSDevTools\Scripts\DocCodeExamples\Config $config Configuration as passed on the + * command line. + * @param \PHPCSDevTools\Scripts\DocCodeExamples\CodeBlocksExtractor $extractor CodeBlocksExtractor instance. + * @param \PHP_CodeSniffer\Config $phpcsConfig PHPCS configuration object. + * @param \PHPCSDevTools\Scripts\Utils\Writer $writer A helper object to write output. + */ + public function __construct( + Config $config, + CodeBlocksExtractor $extractor, + PHPCSConfig $phpcsConfig, + Writer $writer + ) { + $this->config = $config; + $this->extractor = $extractor; + $this->phpcsConfig = $phpcsConfig; + $this->writer = $writer; + + // Handle excluded dirs. + $exclude = '(?!\.git/)'; + if (empty($config->getProperty('excludedDirs')) === false) { + $excludedDirs = \array_map( + 'preg_quote', + $config->getProperty('excludedDirs'), + \array_fill(0, \count($config->getProperty('excludedDirs')), '`') + ); + $exclude = '(?!(\.git|' . \implode('|', $excludedDirs) . ')/)'; + } + + $quotedProjectRoot = \preg_quote($config->getProperty('projectRoot') . '/', '`'); + $docsRegex = \sprintf(self::DOC_FILTER_REGEX, $quotedProjectRoot, $exclude); + + $xmlFilesInDirs = []; + $individualXmlFiles = []; + + foreach ($config->getProperty('targetPaths') as $targetPath) { + if (\is_dir($targetPath)) { + $xmlFilesInDirs[] = (new FileList($targetPath, $config->getProperty('projectRoot'), $docsRegex)) + ->getList(); + } + + if (\is_file($targetPath)) { + $individualXmlFiles[] = \str_replace($config->getProperty('projectRoot'), '', $targetPath); + } + } + + if (empty($xmlFilesInDirs) === false) { + $xmlFilesInDirs = \call_user_func_array('array_merge', $xmlFilesInDirs); + } + + $this->xmlFiles = array_merge($individualXmlFiles, $xmlFilesInDirs); + \sort($this->xmlFiles, \SORT_NATURAL); + } + + /** + * Run the check for sniff documentation code examples. + * + * @return int Exit code for the script. + */ + public function run(): int + { + $this->writer->toStderr($this->config->getVersion()); + + $exitCode = 0; + $validationFailedCount = 0; + + if (empty($this->xmlFiles)) { + $message = 'ERROR: No XML documentation files found.'; + + if ($this->config->getProperty('showColored') === true) { + $message = "\033[31m$message\033[0m"; + } + + $this->writer->toStderr($message . PHP_EOL); + return 1; + } + + foreach ($this->xmlFiles as $xmlDocFilePath) { + $xmlDocValidator = $this->getXmlDocValidator($xmlDocFilePath); + + if (\in_array($xmlDocValidator->sniff, $this->config->getProperty('ignoredSniffs'), true)) { + continue; + } + + $validationSuccessful = $xmlDocValidator->validate(); + + if ($validationSuccessful === false) { + $validationFailedCount++; + $exitCode = 1; + } + } + + $xmlFilesCount = \count($this->xmlFiles); + $feedback = "Checked {$xmlFilesCount} XML documentation files."; + + if ($xmlFilesCount === 1) { + $feedback = 'Checked 1 XML documentation file.'; + } + + if ($exitCode === 0) { + $feedback .= ' All code examples are valid.'; + + if ($this->config->getProperty('showColored') === true) { + $feedback = "\033[32m{$feedback}\033[0m"; + } + } else { + $feedback .= " Found incorrect code examples in {$validationFailedCount}."; + + if ($this->config->getProperty('showColored') === true) { + $feedback = "\033[31m{$feedback}\033[0m"; + } + } + + $this->writer->toStdout($feedback . PHP_EOL); + + return $exitCode; + } + + /** + * Retrieves the XML documentation validator object for a given file path. + * + * @param string $xmlDocFilePath The relative file path to the XML documentation file. + * + * @return \PHPCSDevTools\Scripts\DocCodeExamples\XmlDocValidator The XML documentation validator object. + */ + protected function getXmlDocValidator(string $xmlDocFilePath): XmlDocValidator + { + return new XmlDocValidator( + $this->config->getProperty('projectRoot') . $xmlDocFilePath, + $this->extractor, + $this->phpcsConfig, + $this->writer, + $this->config + ); + } +} diff --git a/Scripts/DocCodeExamples/CodeBlock.php b/Scripts/DocCodeExamples/CodeBlock.php new file mode 100644 index 0000000..52b0519 --- /dev/null +++ b/Scripts/DocCodeExamples/CodeBlock.php @@ -0,0 +1,54 @@ +title = $title; + $this->content = $content; + $this->position = $position; + } +} diff --git a/Scripts/DocCodeExamples/CodeBlocksExtractor.php b/Scripts/DocCodeExamples/CodeBlocksExtractor.php new file mode 100644 index 0000000..ea1ee2b --- /dev/null +++ b/Scripts/DocCodeExamples/CodeBlocksExtractor.php @@ -0,0 +1,126 @@ +> An associative array where the keys are 'valid' + * and 'invalid' and the values are arrays of + * strings with the valid and invalid code blocks. + */ + public function extract(string $xmlDocFilePath): array + { + $this->codeBlocksCounter = 0; + $xmlDoc = $this->loadXmlFile($xmlDocFilePath); + + $codeBlocks = []; + + $xpath = new \DOMXPath($xmlDoc); + $codeBlocks['valid'] = $this->extractCodeBlocksOfGivenType($xpath, 'Valid'); + $codeBlocks['invalid'] = $this->extractCodeBlocksOfGivenType($xpath, 'Invalid'); + + return $codeBlocks; + } + + /** + * Load the XML documentation file and return a DOMDocument object. + * + * @param string $xmlDocFilePath Path to the XML documentation file. + * + * @return \DOMDocument The loaded DOMDocument object. + * + * @throws \RuntimeException If the file is empty or if there are errors while loading the XML. + */ + private function loadXmlFile(string $xmlDocFilePath): \DOMDocument + { + if (\filesize($xmlDocFilePath) === 0) { + throw new \RuntimeException("The {$xmlDocFilePath} file is empty."); + } + + $xmlDoc = new \DOMDocument(); + + libxml_use_internal_errors(true); + $loadResult = $xmlDoc->load($xmlDocFilePath); + + if ($loadResult === false) { + $errors = libxml_get_errors(); + libxml_clear_errors(); + + $errorMessages = []; + foreach ($errors as $error) { + $errorMessages[] = sprintf( + 'Line %d, Column %d: %s', + $error->line, + $error->column, + trim($error->message) + ); + } + + throw new \RuntimeException( + sprintf( + "Failed to parse XML file %s. Errors:\n%s", + $xmlDocFilePath, + implode("\n", $errorMessages) + ) + ); + } + + libxml_use_internal_errors(false); + + return $xmlDoc; + } + + /** + * Extract the code blocks of a given type ('Valid' or 'Invalid') from the XML documentation file. + * + * @param \DOMXPath $xpath The DOMXPath object to use to query the XML document. + * @param string $type The type of code block to extract. Either 'Valid' or 'Invalid'. + * + * @return array The extracted code blocks. + */ + private function extractCodeBlocksOfGivenType(\DOMXPath $xpath, string $type): array + { + $codeBlocks = []; + + $codeBlocksExpression = "//code[starts-with(@title, '{$type}')]"; + $codeBlockElements = $xpath->query($codeBlocksExpression); + + foreach ($codeBlockElements as $codeBlockElement) { + if ($codeBlockElement instanceof \DOMElement === false) { + continue; + } + + $title = $codeBlockElement->getAttribute('title'); + $content = \trim($codeBlockElement->nodeValue); + $content = \preg_replace('``', '', $content); + $codeBlocks[] = new CodeBlock($title, $content, $this->codeBlocksCounter++); + } + + return $codeBlocks; + } +} diff --git a/Scripts/DocCodeExamples/Config.php b/Scripts/DocCodeExamples/Config.php new file mode 100644 index 0000000..52f8119 --- /dev/null +++ b/Scripts/DocCodeExamples/Config.php @@ -0,0 +1,326 @@ + + */ + protected $targetPaths = []; + + /** + * The directories to exclude from the search. + * + * @var array + */ + protected $excludedDirs = []; + + /** + * List of sniffs to ignore. + * + * @var array + */ + protected $ignoredSniffs = []; + + /** + * Whether or not to execute the check. + * + * @var bool + */ + protected $executeCheck = true; + + /** + * Whether or not to show colored output. + * + * This is automatically detected. + * + * @var bool + */ + protected $showColored; + + /** + * Help texts. + * + * @var array>> + */ + private $helpTexts = [ + // phpcs:disable Generic.Files.LineLength.TooLong + 'Usage' => [ + ['text' => 'phpcs-check-doc-examples'], + ['text' => 'phpcs-check-doc-examples [--exclude=] [--ignore-sniffs=] [directories|files]'], + ], + 'Options' => [ + [ + 'arg' => 'directories|files', + 'desc' => 'One or more specific directories or files to examine. Defaults to the directory from which the script is run.', + ], + [ + 'arg' => '--exclude=', + 'desc' => 'Comma-delimited list of relative paths of directories to exclude from the scan.', + ], + [ + 'arg' => '--ignore-sniffs=', + 'desc' => 'Comma-delimited list of sniffs to ignore.', + ], + [ + 'arg' => '--help', + 'desc' => 'Print this help.', + ], + [ + 'arg' => '-V, --version', + 'desc' => 'Display the current version of this script.', + ], + [ + 'arg' => '--colors', + 'desc' => 'Enable colors in console output. (disables auto detection of color support)', + ], + [ + 'arg' => '--no-colors', + 'desc' => 'Disable colors in console output.', + ], + ], + // phpcs:enable + ]; + + /** + * List of supported named arguments. + * + * @var array + */ + private $supportedArgs = [ + '--exclude=', + '--ignore-sniffs=', + '--help', + '-V', + '--version', + '--colors', + '--no-colors', + ]; + + /** + * Constructor. + * + * @param \PHPCSDevTools\Scripts\Utils\Writer $writer Writer for sending output. + */ + public function __construct(Writer $writer) + { + $this->writer = $writer; + $this->processCliCommand(); + } + + /** + * Get the value of a property. + * + * @param string $propertyName The name of the property to retrieve. + * + * @return mixed The value of the property, or null if it doesn't exist. + */ + public function getProperty(string $propertyName) + { + if (isset($this->{$propertyName}) === false) { + throw new \RuntimeException(sprintf('ERROR: Property "%s" does not exist', $propertyName)); + } + + return $this->{$propertyName}; + } + + /** + * Retrieve the version number of this script. + * + * @return string + */ + public function getVersion() + { + $text = 'PHPCSDevTools: XML documentation code examples checker version '; + $text .= \file_get_contents(__DIR__ . '/../../VERSION'); + $text .= \PHP_EOL . 'by PHPCSDevTools Contributors' . \PHP_EOL . \PHP_EOL; + + return $text; + } + + /** + * Process the received command arguments. + * + * @return void + */ + private function processCliCommand() + { + $this->projectRoot = Helper::normalizePath(\getcwd()); + $args = $_SERVER['argv']; + + // Remove the call to the script itself. + \array_shift($args); + + $argsFlipped = \array_flip($args); + + if (isset($argsFlipped['--no-colors'])) { + $this->showColored = false; + } elseif (isset($argsFlipped['--colors'])) { + $this->showColored = true; + } else { + $this->showColored = HelpTextFormatter::isColorSupported(); + } + + if (isset($argsFlipped['--help'])) { + $helpText = HelpTextFormatter::format($this->helpTexts, $this->showColored); + $this->writer->toStdout($this->getVersion()); + $this->writer->toStdout($helpText); + $this->executeCheck = false; + return; + } + + if (isset($argsFlipped['-V']) || isset($argsFlipped['--version'])) { + $this->writer->toStdout($this->getVersion()); + $this->executeCheck = false; + return; + } + + foreach ($args as $arg) { + if (\strpos($arg, '-') === 0 && $this->isUnsupportedNamedArgument($arg) === true) { + throw new \RuntimeException("ERROR: Unsupported argument $arg"); + } + + if (\strpos($arg, '--exclude=') === 0) { + $this->excludedDirs = $this->processExcludeArgument($arg); + continue; + } + + if (\strpos($arg, '--ignore-sniffs=') === 0) { + $this->ignoredSniffs = $this->processIgnoreSniffsArgument($arg); + continue; + } + + if (isset($arg[0]) && $arg[0] !== '-') { + $this->targetPaths[] = $this->processTargetPathArgument($arg); + } + } + + if (empty($this->targetPaths)) { + // If the user didn't provide a path, use the directory from which the script was run. + $this->targetPaths[] = $this->projectRoot; + } + } + + /** + * Check if a named argument is unsupported. + * + * @param string $arg The argument to check. + * + * @return bool True if the argument is unsupported, false otherwise. + */ + private function isUnsupportedNamedArgument(string $arg): bool + { + foreach ($this->supportedArgs as $supportedArg) { + if (\strpos($arg, $supportedArg) === 0) { + return false; + } + } + + return true; + } + + /** + * Process the --exclude argument. + * + * @param string $arg The argument to process. + * + * @return array + */ + protected function processExcludeArgument(string $arg): array + { + $excludeString = \substr($arg, 10); + if (empty($excludeString)) { + return []; + } + + $excludeString = \trim($excludeString, '"\''); // Strip potential quotes. + $excludedDirs = \explode(',', $excludeString); + + $excludedDirs = \array_map( + function ($dir) { + $dir = \trim($dir, '/'); + return Helper::normalizePath($dir); + }, + $excludedDirs + ); + + return $excludedDirs; + } + + /** + * Process the --ignore-sniffs argument. + * + * @param string $arg The argument to process. + * + * @return array + */ + protected function processIgnoreSniffsArgument(string $arg): array + { + $ignoredSniffs = \substr($arg, 16); + if (empty($ignoredSniffs)) { + return []; + } + + $ignoredSniffs = \trim($ignoredSniffs, '"\''); + $ignoredSniffs = \explode(',', $ignoredSniffs); + + return $ignoredSniffs; + } + + /** + * Process the target path argument. + * + * @param string $arg The argument to process. + * + * @return string + * @throws \RuntimeException If the path does not exist. + */ + protected function processTargetPathArgument(string $arg): string + { + $realpath = \realpath($arg); + + if ($realpath === false) { + throw new \RuntimeException(\sprintf('ERROR: Target path %s does not exist', $arg)); + } + + return Helper::normalizePath($realpath); + } +} diff --git a/Scripts/DocCodeExamples/Functions.php b/Scripts/DocCodeExamples/Functions.php new file mode 100644 index 0000000..2fb0168 --- /dev/null +++ b/Scripts/DocCodeExamples/Functions.php @@ -0,0 +1,27 @@ + $files An array of possible file paths. + * + * @return void + */ +function requireCorrectAutoloadFile(array $files) +{ + foreach ($files as $file) { + $file = realpath($file); + if ($file !== false && is_file($file) === true) { + require_once $file; + return; + } + } +} diff --git a/Scripts/DocCodeExamples/Helper.php b/Scripts/DocCodeExamples/Helper.php new file mode 100644 index 0000000..32ea0d5 --- /dev/null +++ b/Scripts/DocCodeExamples/Helper.php @@ -0,0 +1,38 @@ +extractor = $extractor; + $this->phpcsConfig = $phpcsConfig; + $this->writer = $writer; + $this->config = $config; + $this->path = $xmlDocFile; + $pathInfo = \pathinfo($this->path); + + if (\substr($pathInfo['basename'], -12) !== 'Standard.xml') { + throw new \RuntimeException( + "ERROR: The XML file \"{$pathInfo['basename']}\" is invalid. File names should end in \"Standard.xml\"." + ); + } + + // Split the path into directories regardless of which directory separator is used. + $dirs = \preg_split('`[/\\\\]`', $pathInfo['dirname']); + + if (\count($dirs) < 3 || $dirs[\count($dirs) - 2] !== 'Docs') { + throw new \RuntimeException( + 'ERROR: Invalid directory structure in the XML file path. Expected: ' + . '{STANDARD_NAME}/Docs/{CATEGORY_NAME}/' . $pathInfo['basename'] . '.' + ); + } + + $this->standard = $dirs[\count($dirs) - 3]; + $subset = $dirs[\count($dirs) - 1]; + $this->sniff = $this->standard . '.' . $subset . '.' . \str_replace('Standard', '', $pathInfo['filename']); + + if (Standards::isInstalledStandard($this->standard) === false) { + throw new \RuntimeException( + "ERROR: The standard \"{$this->standard}\" is not installed in PHPCS." + ); + } + } + + /** + * Validates all the code blocks in a given XML documentation file. + * + * @return boolean Whether the validation was successful or not. + */ + public function validate(): bool + { + $this->setUpPhpcs(); + + $validated = true; + + $codeBlocks = $this->extractor->extract($this->path); + $validCodeBlocks = $codeBlocks['valid']; + $invalidCodeBlocks = $codeBlocks['invalid']; + + $validPassedValidation = $this->verifyCodeBlocks($validCodeBlocks, true); + $invalidPassedValidation = $this->verifyCodeBlocks($invalidCodeBlocks, false); + + if ($validPassedValidation === false || $invalidPassedValidation === false) { + $validated = false; + } + + return $validated; + } + + /** + * Set up the PHPCS to be able to run the sniff against a code example programmatically. + * + * @return void + */ + private function setUpPhpcs() + { + if (\defined('PHP_CODESNIFFER_VERBOSITY') === false) { + \define('PHP_CODESNIFFER_VERBOSITY', 0); + } + + if (\defined('PHP_CODESNIFFER_CBF') === false) { + \define('PHP_CODESNIFFER_CBF', false); + } + + $this->phpcsConfig->standards = [$this->standard]; + $this->phpcsConfig->sniffs = [$this->sniff]; + + new Tokens(); // @phpstan-ignore new.resultUnused + + $this->phpcsRuleset = new Ruleset($this->phpcsConfig); + } + + /** + * Verify code blocks of a given section (valid or invalid). + * + * @param array $codeBlocks The code blocks to + * validate. + * @param bool $isExpectedToBeValid Whether the code blocks + * are expected to be valid + * or invalid. + * + * @return bool + */ + private function verifyCodeBlocks(array $codeBlocks, bool $isExpectedToBeValid): bool + { + $codeBlocksPassedValidation = true; + + foreach ($codeBlocks as $codeBlock) { + $passedValidation = $this->verifyCodeBlock($codeBlock, $isExpectedToBeValid); + + if ($passedValidation === false && $codeBlocksPassedValidation !== false) { + $codeBlocksPassedValidation = false; + } + } + + return $codeBlocksPassedValidation; + } + + /** + * Run a given group of code examples through PHPCS and check if what is returned matches the + * expectation (whether errors/warnings are expected or not depending on whether the code + * belong to the valid or invalid section). + * + * @param CodeBlock $codeBlock PHP code example to be checked. + * @param bool $isExpectedToBeValid Whether the code example was extracted from the valid + * or invalid section and thus is expected to produce + * PHPCS errors or not. + * + * @return bool Whether the code block matched the expectation. + */ + private function verifyCodeBlock(CodeBlock $codeBlock, bool $isExpectedToBeValid): bool + { + $codeBlockContent = $this->maybeAddPhpOpenTag($codeBlock->content); + $codeBlockBehavesAsExpected = true; + + try { + // @phpstan-ignore function.resultUnused (maybe a PHPStan false positive?) + \token_get_all($codeBlockContent, \TOKEN_PARSE); + } catch (\Throwable $e) { + $message = 'There is a syntax error in the code block.' . \PHP_EOL; + $message .= $e->getMessage() . \PHP_EOL; + $this->displayErrorMessage( + $this->prepareCodeBlockErrorMessage($message, $codeBlock) + ); + $codeBlockBehavesAsExpected = false; + } + + // Set a unique file name for the DummyFile instance to avoid cache conflicts for + // sniffs that rely on PHPCSUtils functionality. + $dummyFileName = md5($this->sniff . $codeBlock->position); + $fileContents = 'phpcs_input_file: ' . $dummyFileName . PHP_EOL; + $fileContents .= $codeBlockContent; + + $file = new DummyFile($fileContents, $this->phpcsRuleset, $this->phpcsConfig); + $file->process(); + + if ($isExpectedToBeValid === true && ($file->getErrorCount() !== 0 || $file->getWarningCount() !== 0)) { + $message = 'Code block is valid and PHPCS should have returned nothing, ' . + 'but instead it returned an error.'; + $this->displayErrorMessage( + $this->prepareCodeBlockErrorMessage($message, $codeBlock) + ); + $codeBlockBehavesAsExpected = false; + } + + if ($isExpectedToBeValid === false && ($file->getErrorCount() === 0 && $file->getWarningCount() === 0)) { + $message = 'Code block is invalid and PHPCS should have returned an error message, ' . + 'but instead it returned nothing.'; + $this->displayErrorMessage( + $this->prepareCodeBlockErrorMessage($message, $codeBlock) + ); + $codeBlockBehavesAsExpected = false; + } + + return $codeBlockBehavesAsExpected; + } + + /** + * Prepare the error message together with the code block title and content. + * + * @param string $message The error message. + * @param CodeBlock $codeBlock The code block that caused the error. + * + * @return string The error message with additional information added. + */ + private function prepareCodeBlockErrorMessage(string $message, CodeBlock $codeBlock): string + { + $errorPrefix = 'ERROR: '; + + if ($this->config->getProperty('showColored') === true) { + $errorPrefix = "\033[31m{$errorPrefix}\033[0m"; + } + + $errorMessage = $errorPrefix . $message . \PHP_EOL; + $errorMessage .= "Code block title: \"{$codeBlock->title}\"" . \PHP_EOL; + $errorMessage .= "Code block content: \"{$codeBlock->content}\"" . \PHP_EOL; + + return $errorMessage . \PHP_EOL; + } + + /** + * Display an error message. Might include the file name at the top, if it is the first + * error message for this XML file. + * + * @param string $message The error message to display. + * + * @return void + */ + private function displayErrorMessage(string $message) + { + if ($this->firstErrorMessageDisplayed === false) { + $errorHeader = "Errors found while processing {$this->path}"; + + if ($this->config->getProperty('showColored') === true) { + $errorHeader = "\033[31m{$errorHeader}\033[0m"; + } + + $this->writer->toStderr($errorHeader . \PHP_EOL . \PHP_EOL); + } + + $this->firstErrorMessageDisplayed = true; + + $this->writer->toStderr($message); + } + + /** + * Adds a PHP open tag to the beginning of the code block content if it doesn't already have one. + * + * @param string $codeBlockContent The code block content to check. + * + * @return string + */ + private function maybeAddPhpOpenTag(string $codeBlockContent): string + { + $normalizedCodeBlockContent = \trim(\strtolower($codeBlockContent)); + + if (\strpos($normalizedCodeBlockContent, 'config = $this->createCompatibleMockBuilderWithMethods(Config::class, []) + ->disableOriginalConstructor() + ->getMock(); + $this->setObjectProperty($this->config, 'showColored', false); + $this->extractor = new CodeBlocksExtractor(); + $cliArgs = ['--runtime-set', 'installed_paths', \realpath(self::STANDARD_DIR)]; + $this->phpcsConfig = PHPCSConfigLoader::getPHPCSConfigInstance($cliArgs); + $this->writer = new TestWriter(); + } + + /** + * Test the constructor method with various target paths and excluded directories + * configurations. + * + * @dataProvider dataConstructorTargets + * + * @param array $targetPaths Paths to set as targets. + * @param array $excludedDirs Dirs to exclude. + * @param array $expectedDocs Expected docs or callback to verify docs. + * + * @return void + */ + public function testConstructorTargets(array $targetPaths, array $excludedDirs, array $expectedDocs) + { + $this->setObjectProperty($this->config, 'projectRoot', Helper::normalizePath(\realpath(self::FIXTURE_DIR))); + $this->setObjectProperty($this->config, 'targetPaths', $targetPaths); + $this->setObjectProperty($this->config, 'excludedDirs', $excludedDirs); + + $check = new Check($this->config, $this->extractor, $this->phpcsConfig, $this->writer); + + $this->assertSame($expectedDocs, $this->getObjectProperty($check, 'xmlFiles')); + } + + /** + * Data provider for testConstructorTargets. + * + * @return array>> + */ + public static function dataConstructorTargets(): array + { + return [ + 'no targets' => [ + 'targetPaths' => [], + 'excludedDirs' => [], + 'expectedDocs' => [], + ], + 'target path does not exist' => [ + 'targetPaths' => [\realpath(self::FIXTURE_DIR . 'nonexistent')], + 'excludedDirs' => [], + 'expectedDocs' => [], + ], + 'no XML files found in target directory' => [ + 'targetPaths' => [\realpath(self::FIXTURE_DIR . '../../DocsXsd')], + 'excludedDirs' => [], + 'expectedDocs' => [], + ], + 'directory target' => [ + 'targetPaths' => [Helper::normalizePath(\realpath(self::FIXTURE_DIR))], + 'excludedDirs' => [], + 'expectedDocs' => [ + '/CheckCodeExamplesStandard/Docs/Constructor/TestXmlDocValidatorConstructorStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/PhpOpenTagStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/PhpcsUtilsCacheStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml', + '/CheckDocPathStandard/Docs/MissingCategoryDirStandard.xml', + '/UninstalledStandard/Docs/Category/TestErrorWhenStandardIsNotInstalledStandard.xml', + ], + ], + 'file target' => [ + 'targetPaths' => [Helper::normalizePath(\realpath(self::FIXTURE_DIR . 'CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml'))], + 'excludedDirs' => [], + 'expectedDocs' => [ + '/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml', + ], + ], + 'multiple targets' => [ + 'targetPaths' => [ + Helper::normalizePath(\realpath(self::FIXTURE_DIR . 'CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml')), + Helper::normalizePath(\realpath(self::FIXTURE_DIR . 'CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml')), + ], + 'excludedDirs' => [], + 'expectedDocs' => [ + '/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml', + ], + ], + 'excluded directories' => [ + 'targetPaths' => [Helper::normalizePath(\realpath(self::FIXTURE_DIR))], + 'excludedDirs' => [ + 'CheckCodeExamplesStandard/Docs', + ], + 'expectedDocs' => [ + '/CheckDocPathStandard/Docs/MissingCategoryDirStandard.xml', + '/UninstalledStandard/Docs/Category/TestErrorWhenStandardIsNotInstalledStandard.xml', + ], + ], + ]; + } + + /** + * Test getXmlDocValidator() method. + * + * @return void + */ + public function testGetXmlDocValidator() + { + $this->setObjectProperty($this->config, 'projectRoot', \realpath(self::FIXTURE_DIR)); + + $check = new Check($this->config, $this->extractor, $this->phpcsConfig, $this->writer); + + $reflection = new \ReflectionClass($check); + $method = $reflection->getMethod('getXmlDocValidator'); + $method->setAccessible(true); + + $result = $method->invokeArgs($check, ['/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml']); + + $method->setAccessible(false); + + $this->assertInstanceOf(XmlDocValidator::class, $result); + } + + /** + * Test the run() method without mocking the Check class. Test only the two happy paths. + * More detailed tests are performed in the testRun() method. + * + * @dataProvider dataRunWithoutMockingCheckClass + * + * @param array $xmlDocs XML doc files to process. + * @param int $expectedExitCode Expected exit code from the run() method. + * @param string $expectedStdout Expected stdout output. + * @param string $expectedStderr Expected stderr output. + * + * @return void + */ + public function testRunWithoutMockingCheckClass(array $xmlDocs, int $expectedExitCode, string $expectedStdout, string $expectedStderr) + { + $this->setObjectProperty($this->config, 'projectRoot', \realpath(self::FIXTURE_DIR)); + $this->setObjectProperty($this->config, 'targetPaths', $xmlDocs); + + $check = new Check($this->config, $this->extractor, $this->phpcsConfig, $this->writer); + + $exitCode = $check->run(); + + $this->assertSame($expectedExitCode, $exitCode); + $this->assertSame($expectedStdout, $this->writer->getStdout()); + $this->assertMatchesRegularExpression('`' . $expectedStderr . '`', $this->writer->getStderr()); + } + + /** + * Data provider for testRunWithoutMockingCheckClass. + * + * @return array>> + */ + public static function dataRunWithoutMockingCheckClass(): array + { + return [ + 'valid XML file' => [ + 'xmlDocs' => [ + \realpath(self::FIXTURE_DIR . 'CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml'), + ], + 'expectedExitCode' => 0, + 'expectedStdout' => 'Checked 1 XML documentation file. All code examples are valid.' . PHP_EOL, + 'expectedStderr' => '', + ], + 'invalid XML file' => [ + 'xmlDocs' => [ + \realpath(self::FIXTURE_DIR . 'CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml'), + ], + 'expectedExitCode' => 1, + 'expectedStdout' => 'Checked 1 XML documentation file. Found incorrect code examples in 1.' . PHP_EOL, + 'expectedStderr' => 'Errors found while processing .*?[/\\\\]CheckCodeExamplesStandard[/\\\\]Docs[/\\\\]Examples[/\\\\]IncorrectInvalidExampleStandard\.xml\R\s*\R\s*ERROR: Code block is invalid and PHPCS should have returned an error message, but instead it returned nothing\.\R\s*Code block title: "Invalid: invalid code examples\."\R\s*Code block content: "function sniffValidationWillPass\(\) \{\}"\R\s*\R', + ], + ]; + } + + /** + * Test the run() method. + * + * @dataProvider dataRun + * + * @param array $xmlDocs XML doc files to process. + * @param array $validationResults Validation results for each doc file. + * @param int $expectedExitCode Expected exit code from the run() method. + * @param string $expectedStdout Expected stdout output. + * @param string $expectedStderr Expected stderr output. + * @param bool $useColors Whether to use colorized output or not. + * + * @return void + */ + public function testRun( + array $xmlDocs, + array $validationResults, + int $expectedExitCode, + string $expectedStdout, + string $expectedStderr, + bool $useColors = false + ) { + $this->setObjectProperty($this->config, 'showColored', $useColors); + + $check = $this->createCompatibleMockBuilderWithMethods(Check::class, ['getXmlDocValidator']) + ->disableOriginalConstructor() + ->getMock(); + + $this->setObjectProperty($check, 'config', $this->config); + $this->setObjectProperty($check, 'xmlFiles', $xmlDocs); + $this->setObjectProperty($check, 'writer', $this->writer); + + // Set up mock XML files with their expected validation results + $mockXmlFiles = []; + foreach ($xmlDocs as $index => $xmlDocPath) { + $mockXmlFile = $this->createMock(XmlDocValidator::class); + $mockXmlFile->expects($this->once()) + ->method('validate') + ->willReturn($validationResults[$index]); + $mockXmlFiles[$xmlDocPath] = $mockXmlFile; + } + + $check->expects($this->exactly(count($xmlDocs))) + ->method('getXmlDocValidator') + ->willReturnCallback(function ($xmlDocPath) use ($mockXmlFiles) { + return $mockXmlFiles[$xmlDocPath]; + }); + + $exitCode = $check->run(); + $this->assertSame($expectedExitCode, $exitCode); + $this->assertSame($expectedStdout, $this->writer->getStdout()); + // Stderr will only contain output from the Check class as the XmlDocValidator class is mocked in this test. + $this->assertSame($this->config->getVersion() . $expectedStderr, $this->writer->getStderr()); + } + + /** + * Data provider for testRun. + * + * @return array> + */ + public static function dataRun(): array + { + return [ + 'no docs' => [ + 'xmlDocs' => [], + 'validationResults' => [], + 'expectedExitCode' => 1, + 'expectedStdout' => '', + 'expectedStderr' => 'ERROR: No XML documentation files found.' . PHP_EOL, + ], + 'all valid docs' => [ + 'xmlDocs' => [ + 'valid1.xml', + 'valid2.xml', + ], + 'validationResults' => [true, true], + 'expectedExitCode' => 0, + 'expectedStdout' => 'Checked 2 XML documentation files. All code examples are valid.' . PHP_EOL, + 'expectedStderr' => '', + ], + 'one invalid and one valid' => [ + 'xmlDocs' => [ + 'valid1.xml', + 'invalid1.xml', + ], + 'validationResults' => [true, false], + 'expectedExitCode' => 1, + 'expectedStdout' => 'Checked 2 XML documentation files. Found incorrect code examples in 1.' . PHP_EOL, + 'expectedStderr' => '', + ], + 'all invalid docs' => [ + 'xmlDocs' => [ + 'invalid1.xml', + 'invalid2.xml', + ], + 'validationResults' => [false, false], + 'expectedExitCode' => 1, + 'expectedStdout' => 'Checked 2 XML documentation files. Found incorrect code examples in 2.' . PHP_EOL, + 'expectedStderr' => '', + ], + 'single doc valid' => [ + 'xmlDocs' => [ + 'valid1.xml', + ], + 'validationResults' => [true], + 'expectedExitCode' => 0, + 'expectedStdout' => 'Checked 1 XML documentation file. All code examples are valid.' . PHP_EOL, + 'expectedStderr' => '', + ], + 'single doc invalid' => [ + 'xmlDocs' => [ + 'invalid1.xml', + ], + 'validationResults' => [false], + 'expectedExitCode' => 1, + 'expectedStdout' => 'Checked 1 XML documentation file. Found incorrect code examples in 1.' . PHP_EOL, + 'expectedStderr' => '', + ], + 'no docs (with colorized output)' => [ + 'xmlDocs' => [], + 'validationResults' => [], + 'expectedExitCode' => 1, + 'expectedStdout' => '', + 'expectedStderr' => "\033[31mERROR: No XML documentation files found.\033[0m" . PHP_EOL, + 'useColors' => true, + ], + 'all valid docs (with colorized output)' => [ + 'xmlDocs' => [ + 'valid1.xml', + 'valid2.xml', + ], + 'validationResults' => [true, true], + 'expectedExitCode' => 0, + 'expectedStdout' => "\033[32mChecked 2 XML documentation files. All code examples are valid.\033[0m" . PHP_EOL, + 'expectedStderr' => '', + 'useColors' => true, + ], + 'one invalid and one valid (with colorized output)' => [ + 'xmlDocs' => [ + 'valid1.xml', + 'invalid1.xml', + ], + 'validationResults' => [true, false], + 'expectedExitCode' => 1, + 'expectedStdout' => "\033[31mChecked 2 XML documentation files. Found incorrect code examples in 1.\033[0m" . PHP_EOL, + 'expectedStderr' => '', + 'useColors' => true, + ], + ]; + } + + /** + * Test that Check::run() correctly ignores sniffs. + * + * @dataProvider dataRunIgnoredSniffs + * + * @param array $xmlDocs List of XML doc file paths. + * @param array $ignoredSniffs List of sniff names to ignore. + * @param array $validateCallMap Map of which files should have validate() called. + * + * @return void + */ + public function testRunIgnoredSniffs(array $xmlDocs, array $ignoredSniffs, array $validateCallMap) + { + $xmlDocValidatorMocks = []; + $projectRoot = \realpath(self::FIXTURE_DIR); + $this->setObjectProperty($this->config, 'projectRoot', $projectRoot); + $this->setObjectProperty($this->config, 'ignoredSniffs', $ignoredSniffs); + + // Create a partial mock to override just the getXmlDocValidator() method. + $check = $this->createCompatibleMockBuilderWithMethods(Check::class, ['getXmlDocValidator']) + ->setConstructorArgs([$this->config, $this->extractor, $this->phpcsConfig, $this->writer]) + ->getMock(); + + foreach ($xmlDocs as $index => $xmlDocPath) { + $xmlDocValidatorMock = $this->createCompatibleMockBuilderWithMethods(XmlDocValidator::class, ['validate']) + ->setConstructorArgs([$projectRoot . $xmlDocPath, $this->extractor, $this->phpcsConfig, $this->writer, $this->config]) + ->getMock(); + $xmlDocValidatorMock->expects($this->exactly($validateCallMap[$index]))->method('validate'); + $xmlDocValidatorMocks[] = $xmlDocValidatorMock; + } + + $check->method('getXmlDocValidator')->willReturnOnConsecutiveCalls(...$xmlDocValidatorMocks); + + $this->setObjectProperty($check, 'xmlFiles', $xmlDocs); + + $check->run(); + } + + /** + * Data provider for testRunIgnoredSniffs. + * + * @return array> + */ + public static function dataRunIgnoredSniffs(): array + { + return [ + 'no ignored sniffs' => [ + 'xmlDocs' => [ + '/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml', + ], + 'ignoredSniffs' => [], + 'validateCallMap' => [1], // File should be validated. + ], + 'all sniffs ignored' => [ + 'xmlDocs' => [ + '/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml', + ], + 'ignoredSniffs' => [ + 'CheckCodeExamplesStandard.Examples.CorrectExamples', + 'CheckCodeExamplesStandard.Examples.IncorrectInvalidExample', + ], + 'validateCallMap' => [0, 0], // No files should be validated. + ], + 'some sniffs ignored' => [ + 'xmlDocs' => [ + '/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml', + '/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard.xml', + ], + 'ignoredSniffs' => ['CheckCodeExamplesStandard.Examples.CorrectExamples'], + 'validateCallMap' => [0, 1, 1], // First file should be skipped. + ], + ]; + } + + /** + * Helper method to create a mock builder of a given class and the set methods to mock in a way + * that's compatible with different PHPUnit versions. + * + * @param class-string $className The name of the class to mock. + * @param array $methods The methods to set. + * + * @return object The created mock builder object. + */ + private function createCompatibleMockBuilderWithMethods(string $className, array $methods) + { + $mockBuilder = $this->getMockBuilder($className); + + // PHPUnit < 9.0 uses setMethods(), newer versions use onlyMethods() + if (\method_exists($mockBuilder, 'setMethods')) { // @phpstan-ignore function.impossibleType + if (empty($methods)) { + $methods = null; + } + return $mockBuilder->setMethods($methods); + } + + return $mockBuilder->onlyMethods($methods); + } +} diff --git a/Tests/DocCodeExamples/CodeBlocksExtractorTest.php b/Tests/DocCodeExamples/CodeBlocksExtractorTest.php new file mode 100644 index 0000000..8f6bdfb --- /dev/null +++ b/Tests/DocCodeExamples/CodeBlocksExtractorTest.php @@ -0,0 +1,164 @@ +expectException(\RuntimeException::class); + $this->expectExceptionMessage($expectedMessage); + + self::$codeBlocksExtractor->extract($filePath); + } + + /** + * Data provider for testExtractCodeBlocksWithInvalidFiles. + * + * @return array> + */ + public static function dataExtractCodeBlocksWithInvalidFiles(): array + { + return [ + 'Empty XML File' => [ + 'filePath' => self::FIXTURE_DIR . 'EmptyFile.xml', + 'expectedMessage' => 'The Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/EmptyFile.xml file is empty.', + ], + 'Invalid XML File' => [ + 'filePath' => self::FIXTURE_DIR . 'InvalidXMLFile.xml', + 'expectedMessage' => "Failed to parse XML file Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/InvalidXMLFile.xml. Errors:\nLine 1, Column 1: Start tag expected, '<' not found", + ], + ]; + } + + /** + * Test that the code blocks are correctly extracted from the XML file. + * + * @dataProvider dataExtractCodeBlocks + * + * @param string $xmlFilePath The path to the XML file to extract code blocks from. + * @param array>|string> $expectedResult The expected result of the extraction. + * + * @return void + */ + public function testExtractCodeBlocks(string $xmlFilePath, array $expectedResult) + { + $codeBlocks = self::$codeBlocksExtractor->extract($xmlFilePath); + + $this->assertEquals($expectedResult, $codeBlocks); + } + + /** + * Data provider for testExtractCodeBlocks. + * + * @return array>|string>> + */ + public static function dataExtractCodeBlocks(): array + { + return [ + 'File with no code blocks' => [ + 'xmlFilePath' => self::FIXTURE_DIR . 'NoCodeBlocks.xml', + 'expectedResult' => [ + 'valid' => [], + 'invalid' => [], + ], + ], + 'One valid block' => [ + 'xmlFilePath' => self::FIXTURE_DIR . 'OneValidBlock.xml', + 'expectedResult' => [ + 'valid' => [ + new CodeBlock( + 'Valid: Example of a valid code block.', + '// Some PHP code.', + 0 + ) + ], + 'invalid' => [], + ], + ], + 'Valid and invalid code blocks' => [ + 'xmlFilePath' => self::FIXTURE_DIR . 'ValidAndInvalidBlocks.xml', + 'expectedResult' => [ + 'valid' => [ + new CodeBlock( + 'Valid: Example of a valid code block.', + '// Some PHP code that does not trigger the sniff.', + 0 + ), + new CodeBlock( + 'Valid: Another example of a valid code block.', + '// Another PHP code that does not trigger the sniff.', + 1 + ), + ], + 'invalid' => [ + new CodeBlock( + 'Invalid: Example of an invalid code block.', + '// Some PHP code that does trigger the sniff.', + 2 + ), + new CodeBlock( + 'Invalid: Another example of an invalid code block.', + '// Another PHP code that does trigger the sniff.', + 3 + ), + ], + ], + ], + ]; + } +} diff --git a/Tests/DocCodeExamples/ConfigTest.php b/Tests/DocCodeExamples/ConfigTest.php new file mode 100644 index 0000000..3113001 --- /dev/null +++ b/Tests/DocCodeExamples/ConfigTest.php @@ -0,0 +1,487 @@ +|bool|null> + */ + private $defaultSettings = [ + 'projectRoot' => '', + 'targetPaths' => [], + 'excludedDirs' => [], + 'ignoredSniffs' => [], + 'executeCheck' => true, + 'showColored' => null, // Defined in setUpPrerequisites() as the default value depends on the environment. + ]; + + /** + * TestWriter instance. + * + * @var \PHPCSDevTools\Tests\TestWriter + */ + private $writer; + + /** + * Set up. + * + * @before + * + * @return void + */ + public function setUpPrerequisites() + { + $this->writer = new TestWriter(); + $this->defaultSettings['showColored'] = HelpTextFormatter::isColorSupported(); + } + + /** + * Test getProperty() throws exception if property does not exist. + * + * @return void + */ + public function testGetPropertyThrowsException() + { + $this->expectException('RuntimeException'); + $this->expectExceptionMessage('Property "NonExistent" does not exist'); + + $_SERVER['argv'] = []; + $config = new Config($this->writer); + $config->getProperty('NonExistent'); + } + + /** + * Test getProperty(). + * + * @dataProvider dataGetProperty + * + * @param string $propertyName The name of the property to retrieve. + * @param mixed $expected The expected value for the property. + * + * @return void + */ + public function testGetProperty($propertyName, $expected) + { + $_SERVER['argv'] = []; + $config = new Config($this->writer); + $this->assertSame($expected, $config->getProperty($propertyName)); + } + + /** + * Data provider. + * + * @return array>> + */ + public static function dataGetProperty(): array + { + $projectRoot = Helper::normalizePath(\getcwd()); + + return [ + 'projectRoot' => [ + 'propertyName' => 'projectRoot', + 'expected' => $projectRoot, + ], + 'targetPaths' => [ + 'propertyName' => 'targetPaths', + 'expected' => [$projectRoot], + ], + 'excludedDirs' => [ + 'propertyName' => 'excludedDirs', + 'expected' => [], + ], + ]; + } + + /** + * Verify that unsupported arguments thrown an exception. + * + * @dataProvider dataProcessCliCommandUnsupportedArgument + * + * @param string $unsupportedArgument The unsupported argument. + * + * @return void + */ + public function testProcessCliCommandUnsupportedArgument(string $unsupportedArgument) + { + $_SERVER['argv'] = \explode(' ', "./phpcs-check-doc-examples $unsupportedArgument"); + + $this->expectException('RuntimeException'); + $this->expectExceptionMessage("Unsupported argument $unsupportedArgument"); + + new Config($this->writer); + } + + /** + * Data provider. + * + * @return array> + */ + public static function dataProcessCliCommandUnsupportedArgument(): array + { + return [ + 'Unsupported short arguments' => [ + 'unsupportedArgument' => '-a', + ], + 'Unsupported long arguments' => [ + 'unsupportedArgument' => '--unsupported-arg', + ], + 'Unsupported long argument using an = sign' => [ + 'unsupportedArgument' => '--ignore=vendor', + ], + ]; + } + + /** + * Verify that an exception is thrown when an invalid target path is passed. + * + * @return void + */ + public function testProcessInvalidTargetThrowException() + { + $this->expectException('RuntimeException'); + $this->expectExceptionMessage('Target path ./doesnotexist does not exist'); + + $_SERVER['argv'] = ['phpcs-check-doc-examples', './doesnotexist']; + new Config($this->writer); + } + + /** + * Test parsing the arguments received from the command line. + * + * @dataProvider dataProcessCliCommand + * + * @param string $command The command as received from the command line. + * @param array> $expectedChanged The Config class properties which are expected + * to have been changed (key) with their value. + * + * @return void + */ + public function testProcessCliCommand(string $command, array $expectedChanged) + { + $expected = \array_merge($this->defaultSettings, $expectedChanged); + + $_SERVER['argv'] = \explode(' ', $command); + $config = new Config($this->writer); + $actual = $this->getCurrentValues($config); + + $this->assertSame($expected, $actual, 'Parsing the command line did not set the properties correctly'); + } + + /** + * Data provider. + * + * @return array>|string>> + */ + public static function dataProcessCliCommand(): array + { + /* + * For project root, we only really verify that it has been set as the value will depend + * on the environment in which the tests are being run. + */ + $projectRoot = Helper::normalizePath(\getcwd()); + + $testData = [ + 'No arguments at all - verify target dir will be set to project root' => [ + 'command' => './phpcs-check-doc-examples', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + ], + ], + 'No arguments at all and trailing whitespace in the command' => [ + 'command' => './phpcs-check-doc-examples ', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + ], + ], + 'No arguments other than a path' => [ + 'command' => './phpcs-check-doc-examples .', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + Helper::normalizePath(\realpath('.')), + ], + ], + ], + 'No arguments other than multiple valid paths in varying formats' => [ + 'command' => './phpcs-check-doc-examples ./PHPCSDebug ./Tests bin ' . __DIR__ . '/../../.github/', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + Helper::normalizePath(\realpath('./PHPCSDebug')), + Helper::normalizePath(\realpath('./Tests')), + Helper::normalizePath(\realpath('bin')), + Helper::normalizePath(\realpath(__DIR__ . '/../../.github/')), + ], + ], + ], + 'Multiple excludes, varying formats' => [ + 'command' => './phpcs-check-doc-examples .' + . ' --exclude=.git,./.github/,Tests/FeatureComplete,/node_modules/,tests/notvalid,../../levelup', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + Helper::normalizePath(\realpath('.')), + ], + 'excludedDirs' => [ + '.git', + './.github', + 'Tests/FeatureComplete', + 'node_modules', + 'tests/notvalid', + '../../levelup', + ], + ], + ], + 'Exclude, complete value wrapped in quotes' => [ + 'command' => './phpcs-check-doc-examples --exclude=".git,./.github/,Tests/FeatureComplete"', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + 'excludedDirs' => [ + '.git', + './.github', + 'Tests/FeatureComplete', + ], + ], + ], + 'Exclude, no value' => [ + 'command' => './phpcs-check-doc-examples --exclude=', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + 'excludedDirs' => [], + ], + ], + 'Single sniff to ignore' => [ + 'command' => './phpcs-check-doc-examples --ignore-sniffs=Generic.CodeAnalysis.EmptyStatement', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + 'ignoredSniffs' => [ + 'Generic.CodeAnalysis.EmptyStatement', + ], + ], + ], + 'Multiple sniffs to ignore wrapped in quotes' => [ + 'command' => "./phpcs-check-doc-examples --ignore-sniffs='Generic.CodeAnalysis.EmptyStatement,Squiz.ControlStructures.ForEachLoopDeclaration,Squiz.Arrays.ArrayDeclaration'", + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + 'ignoredSniffs' => [ + 'Generic.CodeAnalysis.EmptyStatement', + 'Squiz.ControlStructures.ForEachLoopDeclaration', + 'Squiz.Arrays.ArrayDeclaration', + ], + ], + ], + 'All together now, includes testing for handling of additional whitespace between arguments' => [ + 'command' => 'phpcs-check-doc-examples Scripts --exclude=ignoreme,/other,./tests/' + . ' --ignore-sniffs=Generic.CodeAnalysis.EmptyStatement PHPCSDebug ./Tests .', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + Helper::normalizePath(\realpath('Scripts')), + Helper::normalizePath(\realpath('PHPCSDebug')), + Helper::normalizePath(\realpath('./Tests')), + Helper::normalizePath(\realpath('.')), + ], + 'excludedDirs' => [ + 'ignoreme', + 'other', + './tests', + ], + 'ignoredSniffs' => [ + 'Generic.CodeAnalysis.EmptyStatement', + ], + ], + ], + 'Help argument' => [ + 'command' => './phpcs-check-doc-examples --help', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'executeCheck' => false, + ], + ], + 'Version argument short' => [ + 'command' => './phpcs-check-doc-examples -V', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'executeCheck' => false, + ], + ], + 'Version argument long' => [ + 'command' => './phpcs-check-doc-examples --version', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'executeCheck' => false, + ], + ], + 'Colors argument' => [ + 'command' => './phpcs-check-doc-examples --colors', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + 'showColored' => true, + ], + ], + 'No colors argument' => [ + 'command' => './phpcs-check-doc-examples --no-colors', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + 'showColored' => false, + ], + ], + 'Both colors and no-colors arguments (no-colors should win)' => [ + 'command' => './phpcs-check-doc-examples --no-colors --colors', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + 'showColored' => false, + ], + ], + 'Both colors and no-colors arguments in reverse order (no-colors should win)' => [ + 'command' => './phpcs-check-doc-examples --colors --no-colors', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + $projectRoot, + ], + 'showColored' => false, + ], + ], + ]; + + // Windows only test: verify that the paths are normalized to use forward slashes. + if (\DIRECTORY_SEPARATOR === '\\') { + $testData['Target paths and excluded dirs paths normalized to use forward slashes'] = [ + 'command' => 'phpcs-check-doc-examples Scripts\DocCodeExamples Tests\Fixtures\DocCodeExamples' + . ' --exclude=Scripts\Utils,Tests\Fixtures\DocsXsd', + 'expectedChanged' => [ + 'projectRoot' => $projectRoot, + 'targetPaths' => [ + Helper::normalizePath(\realpath('Scripts/DocCodeExamples')), + Helper::normalizePath(\realpath('Tests/Fixtures/DocCodeExamples')), + ], + 'excludedDirs' => [ + 'Scripts/Utils', + 'Tests/Fixtures/DocsXsd', + ], + ], + ]; + } + + return $testData; + } + + /** + * Test that the help command outputs the expected text. + * + * @return void + */ + public function testHelpOutput() + { + $_SERVER['argv'] = ['phpcs-check-doc-examples', '--help']; + new Config($this->writer); + + $output = $this->writer->getStdout(); + + // Just check that a key element in the help text is displayed. + $this->assertStringContainsString('Usage:', $output); + $this->assertStringContainsString('phpcs-check-doc-examples', $output); + } + + /** + * Test that the version command outputs the expected text. + * + * @dataProvider dataVersionOutput + * + * @param string $command The command to run. + * + * @return void + */ + public function testVersionOutput($command) + { + $_SERVER['argv'] = \explode(' ', $command); + new Config($this->writer); + + $this->assertMatchesRegularExpression(VersionTestUtils::VERSION_HEADER_REGEX, $this->writer->getStdout()); + } + + /** + * Data provider for the testVersionOutput method. + * + * @return array> + */ + public static function dataVersionOutput() + { + return [ + '-V' => [ + 'command' => 'phpcs-check-doc-examples -V', + ], + '--version' => [ + 'command' => 'phpcs-check-doc-examples --version', + ], + ]; + } + + /** + * Helper method: retrieve the current values of the Config properties as an array. + * + * @param Config $config Config object + * + * @return array> + */ + private function getCurrentValues(Config $config): array + { + $current = []; + foreach ($this->defaultSettings as $name => $value) { + $current[$name] = $config->getProperty($name); + } + + return $current; + } +} diff --git a/Tests/DocCodeExamples/EndToEndTest.php b/Tests/DocCodeExamples/EndToEndTest.php new file mode 100644 index 0000000..cb4b8cc --- /dev/null +++ b/Tests/DocCodeExamples/EndToEndTest.php @@ -0,0 +1,186 @@ +executeCliCommand($command); + + $this->assertSame($expectedExitCode, $result['exitcode']); + + $this->assertSame($expectedStdout, $result['stdout']); + + if (empty($expectedStderr)) { + $this->assertStderrContainsOnlyScriptHeader($result['stderr']); + } else { + $this->assertMatchesRegularExpression($expectedStderr, $result['stderr']); + } + } + + /** + * Assert that stderr contains only the script header and nothing else. + * + * @param string $stderr The stderr output to check. + * + * @return void + */ + protected function assertStderrContainsOnlyScriptHeader(string $stderr) + { + $this->assertMatchesRegularExpression(VersionTestUtils::VERSION_HEADER_REGEX, $stderr); + } + + /** + * Data provider for testScriptBasicExecution. + * + * @return array> + */ + public static function dataScriptBasicExecution(): array + { + return [ + 'Valid XML file' => [ + 'cliArgs' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml', + 'expectedExitCode' => 0, + 'expectedStdout' => 'Checked 1 XML documentation file. All code examples are valid.' . PHP_EOL, + 'expectedStderr' => '', + ], + 'Directory with valid XML files (invalid files ignored via --ignore-sniffs)' => [ + 'cliArgs' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/ --ignore-sniffs=CheckCodeExamplesStandard.Constructor.TestXmlDocValidatorConstructor,CheckCodeExamplesStandard.Examples.IncorrectInvalidExample,CheckCodeExamplesStandard.Examples.IncorrectValidExample,CheckCodeExamplesStandard.Examples.SyntaxErrorExample', + 'expectedExitCode' => 0, + 'expectedStdout' => 'Checked 7 XML documentation files. All code examples are valid.' . PHP_EOL, + 'expectedStderr' => '', + ], + 'Invalid XML file' => [ + 'cliArgs' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard.xml', + 'expectedExitCode' => 1, + 'expectedStdout' => 'Checked 1 XML documentation file. Found incorrect code examples in 1.' . PHP_EOL, + 'expectedStderr' => '`Errors found while processing .*?Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard\.xml`', + ], + 'Directory with valid and invalid XML files (empty file ignored via --ignore-sniffs)' => [ + 'cliArgs' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/ --ignore-sniffs=CheckCodeExamplesStandard.Constructor.TestXmlDocValidatorConstructor', + 'expectedExitCode' => 1, + 'expectedStdout' => 'Checked 7 XML documentation files. Found incorrect code examples in 3.' . PHP_EOL, + 'expectedStderr' => '`Errors found while processing .*?Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard\.xml`', + ], + ]; + } + + /** + * If on Windows, convert forward slashes to backslashes so that paths work. + * + * @param string $path The path to convert. + * + * @return string The converted path. + */ + public static function maybeConvertDirectorySeparators($path): string + { + if (DIRECTORY_SEPARATOR === '\\') { + return str_replace('/', '\\', $path); + } + + return $path; + } +} diff --git a/Tests/DocCodeExamples/HelperTest.php b/Tests/DocCodeExamples/HelperTest.php new file mode 100644 index 0000000..1871d52 --- /dev/null +++ b/Tests/DocCodeExamples/HelperTest.php @@ -0,0 +1,65 @@ +assertSame($expected, Helper::normalizePath($path)); + } + + /** + * Data provider for testing normalizePath(). + * + * @return array> + */ + public static function dataNormalizePath() + { + return [ + 'Windows-style path' => [ + 'path' => 'C:\\path\\to\\file.txt', + 'expected' => 'C:/path/to/file.txt', + ], + 'Unix-style path' => [ + 'path' => '/path/to/file.txt', + 'expected' => '/path/to/file.txt', + ], + 'Mixed path' => [ + 'path' => '/path\\to/file\\txt', + 'expected' => '/path/to/file/txt', + ], + 'Empty string' => [ + 'path' => '', + 'expected' => '', + ], + ]; + } +} diff --git a/Tests/DocCodeExamples/ManipulateObjectsTrait.php b/Tests/DocCodeExamples/ManipulateObjectsTrait.php new file mode 100644 index 0000000..54b8620 --- /dev/null +++ b/Tests/DocCodeExamples/ManipulateObjectsTrait.php @@ -0,0 +1,57 @@ +getProperty($propertyName); + $property->setAccessible(true); + $property->setValue($object, $value); + $property->setAccessible(false); + } + + /** + * Get a private/protected property from an object. + * + * @param object $object The object to get the property from. + * @param string $propertyName The name of the property to retrieve. + * + * @return mixed + */ + private function getObjectProperty($object, string $propertyName) + { + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty($propertyName); + $property->setAccessible(true); + $value = $property->getValue($object); + $property->setAccessible(false); + + return $value; + } +} diff --git a/Tests/DocCodeExamples/PHPCSConfigLoader.php b/Tests/DocCodeExamples/PHPCSConfigLoader.php new file mode 100644 index 0000000..31a7907 --- /dev/null +++ b/Tests/DocCodeExamples/PHPCSConfigLoader.php @@ -0,0 +1,40 @@ += 3.9.0 + * or the regular PHPCS Config class when running the tests with PHPCS < 3.9.0. + */ +class PHPCSConfigLoader +{ + + /** + * Returns an instance of the PHPCS ConfigDouble class or the PHPCS Config class when the former + * is not available. Necessary when running the tests with PHPCS < 3.9.0 as the ConfigDouble + * class is not available in those versions. + * + * @param array $cliArgs CLI arguments to pass to the PHPCS Config/ConfigDouble class. + * + * @return \PHP_CodeSniffer\Config|\PHP_CodeSniffer\Tests\ConfigDouble + */ + public static function getPHPCSConfigInstance(array $cliArgs): PHPCSConfig + { + if (\class_exists('PHP_CodeSniffer\Tests\ConfigDouble') === false) { + return new PHPCSConfig($cliArgs); + } + + return new PHPCSConfigDouble($cliArgs); + } +} diff --git a/Tests/DocCodeExamples/VersionTestUtils.php b/Tests/DocCodeExamples/VersionTestUtils.php new file mode 100644 index 0000000..8f43f68 --- /dev/null +++ b/Tests/DocCodeExamples/VersionTestUtils.php @@ -0,0 +1,21 @@ +extractor = new CodeBlocksExtractor(); + $cliArgs = ['--runtime-set', 'installed_paths', \realpath(self::STANDARD_DIR)]; + $this->phpcsConfig = PHPCSConfigLoader::getPHPCSConfigInstance($cliArgs); + $this->writer = new TestWriter(); + $_SERVER['argv'] = []; + $this->config = new Config($this->writer); + } + + /** + * Test that the constructor throws exceptions for various invalid inputs. + * + * @dataProvider dataConstructorExceptions + * + * @param string $filePath Path to the file to test with. + * @param string $expectedMessage Expected exception message. + * + * @return void + */ + public function testConstructorThrowsExceptions($filePath, $expectedMessage) + { + $this->expectException('RuntimeException'); + $this->expectExceptionMessage($expectedMessage); + + new XmlDocValidator($filePath, $this->extractor, $this->phpcsConfig, $this->writer, $this->config); + } + + /** + * Data provider for testing constructor exceptions. + * + * @return array> + */ + public static function dataConstructorExceptions() + { + return [ + 'non-existent file' => [ + 'filePath' => '/non/existent/file.xml', + 'expectedMessage' => 'The XML file "/non/existent/file.xml" does not exist.', + ], + 'invalid file extension' => [ + 'filePath' => __DIR__ . '/../Fixtures/DocCodeExamples/CheckDocPathStandard/Docs/Category/SniffName.txt', + 'expectedMessage' => 'The XML file "SniffName.txt" is invalid. File names should end in "Standard.xml".', + ], + 'invalid directory structure: missing category dir' => [ + 'filePath' => __DIR__ . '/../Fixtures/DocCodeExamples/CheckDocPathStandard/Docs/MissingCategoryDirStandard.xml', + 'expectedMessage' => 'Invalid directory structure in the XML file path. Expected: {STANDARD_NAME}/Docs/{CATEGORY_NAME}/MissingCategoryDirStandard.xml.', + ], + 'invalid directory structure: missing sniffs dir' => [ + 'filePath' => __DIR__ . '/../Fixtures/DocCodeExamples/CheckDocPathStandard/Category/MissingDocsDirStandard.xml', + 'expectedMessage' => 'Invalid directory structure in the XML file path. Expected: {STANDARD_NAME}/Docs/{CATEGORY_NAME}/MissingDocsDirStandard.xml.', + ], + 'standard is not installed in PHPCS' => [ + 'filePath' => __DIR__ . '/../Fixtures/DocCodeExamples/UninstalledStandard/Docs/Category/TestErrorWhenStandardIsNotInstalledStandard.xml', + 'expectedMessage' => 'The standard "UninstalledStandard" is not installed in PHPCS.', + ], + ]; + } + + /** + * Test that the constructor correctly sets the properties. + * + * @return void + */ + public function testConstructor() + { + $fixturePath = __DIR__ . '/../Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Constructor/TestXmlDocValidatorConstructorStandard.xml'; + + $xmlDocFile = new XmlDocValidator($fixturePath, $this->extractor, $this->phpcsConfig, $this->writer, $this->config); + + $this->assertSame($fixturePath, $xmlDocFile->path); + $this->assertSame('CheckCodeExamplesStandard', $xmlDocFile->standard); + $this->assertSame('CheckCodeExamplesStandard.Constructor.TestXmlDocValidatorConstructor', $xmlDocFile->sniff); + } + + /** + * Test the validate() method. It should return 0 when the code examples match the + * expectation (an error when it is an invalid example and no errors when it is a valid example) + * and 1 when it doesn't. It should also output any errors found. + * + * @param string $xmlPath The path to a sniff documentation XML file. + * @param string $expectedErrorMessage The expected error messages. + * @param int $expectedReturnValue The expected return value. + * @param bool $useColor Whether to use color in the output. + * + * @dataProvider dataValidate + * + * @return void + */ + public function testValidate($xmlPath, $expectedErrorMessage, $expectedReturnValue, $useColor = false) + { + $this->setObjectProperty($this->config, 'showColored', $useColor); + $xmlDocValidator = new XmlDocValidator($xmlPath, $this->extractor, $this->phpcsConfig, $this->writer, $this->config); + + $this->assertSame($expectedReturnValue, $xmlDocValidator->validate()); + $this->assertSame($expectedErrorMessage, $this->writer->getStderr()); + } + + /** + * Data provider. + * + * @return array> + */ + public static function dataValidate() + { + return [ + 'All code examples match the expectation' => [ + 'xmlPath' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml', + 'expectedErrorMessage' => '', + 'expectedReturnValue' => true, + ], + 'One valid code example which is actually invalid' => [ + 'xmlPath' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard.xml', + 'expectedErrorMessage' => 'Errors found while processing Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard.xml' . PHP_EOL . PHP_EOL . + 'ERROR: Code block is valid and PHPCS should have returned nothing, but instead it returned an error.' . PHP_EOL . + 'Code block title: "Valid: invalid valid code example."' . PHP_EOL . + 'Code block content: "function sniffValidationWillFail() {}"' . PHP_EOL . PHP_EOL, + 'expectedReturnValue' => false, + ], + 'One invalid code example which is actually valid' => [ + 'xmlPath' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml', + 'expectedErrorMessage' => 'Errors found while processing Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml' . PHP_EOL . PHP_EOL . + 'ERROR: Code block is invalid and PHPCS should have returned an error message, but instead it returned nothing.' . PHP_EOL . + 'Code block title: "Invalid: invalid code examples."' . PHP_EOL . + 'Code block content: "function sniffValidationWillPass() {}"' . PHP_EOL . PHP_EOL, + 'expectedReturnValue' => false, + ], + 'Code example with syntax error' => [ + 'xmlPath' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml', + 'expectedErrorMessage' => 'Errors found while processing Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml' . PHP_EOL . PHP_EOL . + 'ERROR: There is a syntax error in the code block.' . PHP_EOL . + 'syntax error, unexpected end of file' . PHP_EOL . PHP_EOL . + 'Code block title: "Valid: syntax error example."' . PHP_EOL . + 'Code block content: "sniffValidationWillPass() // Syntax error: missing semicolon."' . PHP_EOL . PHP_EOL . + 'ERROR: There is a syntax error in the code block.' . PHP_EOL . + 'syntax error, unexpected end of file' . PHP_EOL . PHP_EOL . + 'Code block title: "Invalid: syntax error example."' . PHP_EOL . + 'Code block content: "sniffValidationWillFail() // Syntax error: missing semicolon."' . PHP_EOL . PHP_EOL, + 'expectedReturnValue' => false, + ], + 'Code example from a sniff that uses PHPCSUtils caching' => [ + 'xmlPath' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpcsUtilsCacheStandard.xml', + 'expectedErrorMessage' => '', + 'expectedReturnValue' => true, + ], + 'Code examples testing adding or not the PHP open tag' => [ + 'xmlPath' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpOpenTagStandard.xml', + 'expectedErrorMessage' => '', + 'expectedReturnValue' => true, + ], + 'Code example with syntax error using colorized output' => [ + 'xmlPath' => 'Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml', + 'expectedErrorMessage' => "\033[31mErrors found while processing Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml\033[0m" . PHP_EOL . PHP_EOL . + "\033[31mERROR: \033[0mThere is a syntax error in the code block." . PHP_EOL . + 'syntax error, unexpected end of file' . PHP_EOL . PHP_EOL . + 'Code block title: "Valid: syntax error example."' . PHP_EOL . + 'Code block content: "sniffValidationWillPass() // Syntax error: missing semicolon."' . PHP_EOL . PHP_EOL . + "\033[31mERROR: \033[0mThere is a syntax error in the code block." . PHP_EOL . + 'syntax error, unexpected end of file' . PHP_EOL . PHP_EOL . + 'Code block title: "Invalid: syntax error example."' . PHP_EOL . + 'Code block content: "sniffValidationWillFail() // Syntax error: missing semicolon."' . PHP_EOL . PHP_EOL, + 'expectedReturnValue' => false, + 'useColor' => true, + ], + ]; + } +} diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Constructor/TestXmlDocValidatorConstructorStandard.xml b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Constructor/TestXmlDocValidatorConstructorStandard.xml new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml new file mode 100644 index 0000000..06c6e8e --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/CorrectExamplesStandard.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + block containing valid and invalid code examples that are correct. The valid + examples don't violate the sniff, and the invalid examples do. + ]]> + + + + + + + + + + diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml new file mode 100644 index 0000000..b925091 --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectInvalidExampleStandard.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard.xml b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard.xml new file mode 100644 index 0000000..8ece159 --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/IncorrectValidExampleStandard.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpOpenTagStandard.xml b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpOpenTagStandard.xml new file mode 100644 index 0000000..d8728db --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpOpenTagStandard.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + ]]> + + +

+ ]]> +
+
+ + +

+ ]]> +
+
+
diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpcsUtilsCacheStandard.xml b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpcsUtilsCacheStandard.xml new file mode 100644 index 0000000..964c30d --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/PhpcsUtilsCacheStandard.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml new file mode 100644 index 0000000..24b94d3 --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Docs/Examples/SyntaxErrorExampleStandard.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/BaseExamplesSniff.php b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/BaseExamplesSniff.php new file mode 100644 index 0000000..4a31cb0 --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/BaseExamplesSniff.php @@ -0,0 +1,28 @@ +getTokens()[$stackPtr]['content'] === 'sniffValidationWillFail') { + $phpcsFile->addError('This is a error', $stackPtr, 'Error'); + } + } +} diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/CorrectExamplesSniff.php b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/CorrectExamplesSniff.php new file mode 100644 index 0000000..198855e --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/CorrectExamplesSniff.php @@ -0,0 +1,11 @@ +addError('This is a error', $stackPtr, 'TestError'); + } + } + +} diff --git a/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/SyntaxErrorExampleSniff.php b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/SyntaxErrorExampleSniff.php new file mode 100644 index 0000000..2ad10ed --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckCodeExamplesStandard/Sniffs/Examples/SyntaxErrorExampleSniff.php @@ -0,0 +1,11 @@ + + + + diff --git a/Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Category/MissingDocsDirStandard.xml b/Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Category/MissingDocsDirStandard.xml new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Docs/Category/SniffName.txt b/Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Docs/Category/SniffName.txt new file mode 100644 index 0000000..7630413 --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Docs/Category/SniffName.txt @@ -0,0 +1 @@ +XML documentation files should end with the "Standard.xml" suffix diff --git a/Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Docs/MissingCategoryDirStandard.xml b/Tests/Fixtures/DocCodeExamples/CheckDocPathStandard/Docs/MissingCategoryDirStandard.xml new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/EmptyFile.xml b/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/EmptyFile.xml new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/InvalidXMLFile.xml b/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/InvalidXMLFile.xml new file mode 100644 index 0000000..41318be --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/InvalidXMLFile.xml @@ -0,0 +1 @@ +Not a valid XML file. diff --git a/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/NoCodeBlocks.xml b/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/NoCodeBlocks.xml new file mode 100644 index 0000000..c3e7fe2 --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/NoCodeBlocks.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/OneValidBlock.xml b/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/OneValidBlock.xml new file mode 100644 index 0000000..2a1d851 --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/OneValidBlock.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/ValidAndInvalidBlocks.xml b/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/ValidAndInvalidBlocks.xml new file mode 100644 index 0000000..e0b57fe --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/CodeBlocksExtractor/ValidAndInvalidBlocks.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + block in this test file with valid and invalid code blocks. + ]]> + + + + + + + + + + diff --git a/Tests/Fixtures/DocCodeExamples/InvalidPathToDocStandard.xml b/Tests/Fixtures/DocCodeExamples/InvalidPathToDocStandard.xml new file mode 100644 index 0000000..65ea34f --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/InvalidPathToDocStandard.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/Tests/Fixtures/DocCodeExamples/UninstalledStandard/Docs/Category/TestErrorWhenStandardIsNotInstalledStandard.xml b/Tests/Fixtures/DocCodeExamples/UninstalledStandard/Docs/Category/TestErrorWhenStandardIsNotInstalledStandard.xml new file mode 100644 index 0000000..9039a1c --- /dev/null +++ b/Tests/Fixtures/DocCodeExamples/UninstalledStandard/Docs/Category/TestErrorWhenStandardIsNotInstalledStandard.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/bin/phpcs-check-doc-examples b/bin/phpcs-check-doc-examples new file mode 100644 index 0000000..c47f188 --- /dev/null +++ b/bin/phpcs-check-doc-examples @@ -0,0 +1,87 @@ +#!/usr/bin/env php +getProperty('executeCheck') === false) { + // This was a help request. + exit(0); + } + + $extractor = new PHPCSDevTools\Scripts\DocCodeExamples\CodeBlocksExtractor(); + + // It is necessary to reset argv to avoid issues with the PHPCS Config, + // as it also checks for command line arguments. + $_SERVER['argv'] = []; + $phpcsConfig = new PHP_CodeSniffer\Config(); + + $check = new PHPCSDevTools\Scripts\DocCodeExamples\Check($config, $extractor, $phpcsConfig, $writer); + exit($check->run()); +} catch (RuntimeException $e) { + $message = $e->getMessage(); + + if ($config->getProperty('showColored') === true) { + $message = "\033[31m{$message}\033[0m"; + } + + echo $message . PHP_EOL; + exit(1); +} diff --git a/composer.json b/composer.json index 8feec7b..e0f6f31 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,8 @@ } }, "bin": [ - "bin/phpcs-check-feature-completeness" + "bin/phpcs-check-feature-completeness", + "bin/phpcs-check-doc-examples" ], "minimum-stability": "dev", "prefer-stable": true, @@ -63,6 +64,9 @@ "lintlt72": [ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --show-deprecated --exclude vendor --exclude .git --exclude .github/build" ], + "lintlt70": [ + "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --show-deprecated --exclude vendor --exclude .git --exclude .github/build --exclude Scripts/DocCodeExamples --exclude Tests/DocCodeExamples" + ], "checkcs": [ "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" ], @@ -78,6 +82,9 @@ "test-tools": [ "@php ./vendor/phpunit/phpunit/phpunit --testsuite DevTools" ], + "test-doc-code-examples": [ + "@php ./vendor/phpunit/phpunit/phpunit --testsuite DocCodeExamples" + ], "test-lte9": [ "@php ./vendor/phpunit/phpunit/phpunit -c phpunitlte9.xml.dist" ], @@ -87,6 +94,9 @@ "test-tools-lte9": [ "@php ./vendor/phpunit/phpunit/phpunit -c phpunitlte9.xml.dist --testsuite DevTools" ], + "test-doc-code-examples-lte9": [ + "@php ./vendor/phpunit/phpunit/phpunit -c phpunitlte9.xml.dist --testsuite DocCodeExamples" + ], "check-complete": [ "@php ./bin/phpcs-check-feature-completeness ./PHPCSDebug" ] diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 426a5c5..f524727 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -62,4 +62,90 @@ /Tests/*\.php$ + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + + + /Scripts/DocCodeExamples/*\.php$ + /Tests/DocCodeExamples/*\.php$ + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ed1e1a0..716fe8b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -30,5 +30,8 @@ ./Tests/FeatureComplete/ ./Tests/Utils/ + + ./Tests/DocCodeExamples/ + diff --git a/phpunitlte9.xml.dist b/phpunitlte9.xml.dist index 7a37473..9a7b955 100644 --- a/phpunitlte9.xml.dist +++ b/phpunitlte9.xml.dist @@ -21,5 +21,8 @@ ./Tests/FeatureComplete/ ./Tests/Utils/ + + ./Tests/DocCodeExamples/ +