diff --git a/.gitattributes b/.gitattributes index 30f7d6a..f9118ff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,10 @@ /.gitattributes export-ignore /.gitignore export-ignore /.travis.yml export-ignore +/phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore +/phpunit-bootstrap.php export-ignore +/PHPCSDebug/Tests export-ignore # # Auto detect text files and perform LF normalization diff --git a/.gitignore b/.gitignore index d1502b0..63850bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ vendor/ composer.lock +.phpcs.xml +phpcs.xml +phpunit.xml +.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml index 5fc495a..affe683 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ language: php ## Cache composer and apt downloads. cache: + apt: true directories: # Cache directory for older Composer versions. - $HOME/.composer/cache/files @@ -12,21 +13,142 @@ cache: php: - 5.4 - - 7.3 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + +env: + # `master` + - PHPCS_VERSION="dev-master" LINT=1 + # Lowest supported PHPCS version. + - PHPCS_VERSION="3.0.2" + +# Define the stages used. +# For non-PRs, only the sniff and quicktest stages are run. +# For pull requests and merges, the full script is run (skipping quicktest). +# Note: for pull requests, "develop" is the base branch name. +# See: https://docs.travis-ci.com/user/conditions-v1 +stages: + - name: sniff + - name: quicktest + if: type = push AND branch NOT IN (master, develop) + - name: test + if: branch IN (master, develop) jobs: fast_finish: true + include: + #### SNIFF STAGE #### + - stage: sniff + php: 7.3 + env: PHPCS_VERSION="dev-master" + addons: + apt: + packages: + - libxml2-utils + script: + # Check the code style of the code base. + - composer check-cs + + # Validate the xml file. + # @link http://xmlsoft.org/xmllint.html + - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./PHPCSDebug/ruleset.xml + + # Check the code-style consistency of the xml files. + - diff -B ./PHPCSDebug/ruleset.xml <(xmllint --format "./PHPCSDebug/ruleset.xml") + + # Validate the composer.json file. + # @link https://getcomposer.org/doc/03-cli.md#validate + - composer validate --no-check-all --strict + + #### QUICK TEST STAGE #### + # This is a much quicker test which only runs the unit tests and linting against the low/high + # supported PHP/PHPCS combinations. + - stage: quicktest + php: 7.3 + env: PHPCS_VERSION="dev-master" LINT=1 + - stage: quicktest + php: 7.2 + env: PHPCS_VERSION="3.0.2" + + - stage: quicktest + php: 5.4 + env: PHPCS_VERSION="dev-master" LINT=1 + - stage: quicktest + php: 5.4 + env: PHPCS_VERSION="3.0.2" + + #### TEST STAGE #### + # Additional builds to prevent issues with PHPCS versions incompatible with certain PHP versions. + - stage: test + php: 7.3 + env: PHPCS_VERSION="dev-master" LINT=1 + # PHPCS is only compatible with PHP 7.3 as of version 3.3.1. + - php: 7.3 + env: PHPCS_VERSION="3.3.1" + - php: 7.4 + env: PHPCS_VERSION="dev-master" + # PHPCS is only compatible with PHP 7.4 as of version 3.5.0. + - php: 7.4 + env: PHPCS_VERSION="3.5.0" + - php: "nightly" + env: PHPCS_VERSION="n/a" LINT=1 + + allow_failures: + # Allow failures for unstable builds. + - php: "nightly" before_install: # Speed up build time by disabling Xdebug when its not needed. - phpenv config-rm xdebug.ini || echo 'No xdebug config.' + # 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 [[ "$TRAVIS_BUILD_STAGE_NAME" != "Sniff" && "$PHPCS_VERSION" != "dev-master" && "$PHPCS_VERSION" != "n/a" ]]; then + echo 'error_reporting = E_ALL & ~E_DEPRECATED' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini + fi + + - export XMLLINT_INDENT=" " + + # Set up test environment using Composer. + - | + if [[ "$TRAVIS_BUILD_STAGE_NAME" != "Sniff" ]]; then + # Remove the PHPCSDevCS dependency as it has different PHPCS requirements and would block installs. + composer remove --dev phpcsstandards/phpcsdevcs --no-update --no-scripts + fi + - | + if [[ $PHPCS_VERSION != "n/a" ]]; then + composer require --no-update --no-scripts squizlabs/php_codesniffer:${PHPCS_VERSION} + fi + - | + if [[ "$TRAVIS_BUILD_STAGE_NAME" == "Sniff" || $PHPCS_VERSION == "n/a" ]]; then + # The sniff stage doesn't run the unit tests, so no need for PHPUnit. + composer remove --dev phpunit/phpunit --no-update --no-scripts + elif [[ "$PHPCS_VERSION" < "3.1.0" ]]; then + # PHPCS < 3.1.0 is not compatible with PHPUnit 6.x. + composer require --dev phpunit/phpunit:"^4.0||^5.0" --no-update --no-scripts + elif [[ "$PHPCS_VERSION" < "3.2.3" ]]; then + # PHPCS < 3.2.3 is not compatible with PHPUnit 7.x. + composer require --dev phpunit/phpunit:"^4.0||^5.0||^6.0" --no-update --no-scripts + fi + # --prefer-dist will allow for optimal use of the travis caching ability. + # The Composer PHPCS plugin takes care of setting the installed_paths for PHPCS. - composer install --prefer-dist --no-suggest script: - # Validate the composer.json file on low/high PHP versions. - # @link https://getcomposer.org/doc/03-cli.md#validate - - composer validate --no-check-all --strict + # Lint PHP files against parse errors. + - if [[ "$LINT" == "1" ]]; then composer lint; fi + + # Check that any sniffs available are feature complete. + # This also acts as an integration test for the feature completeness script, + # which is why it is run against various PHP versions and not in the "Sniff" stage. + - if [[ "$LINT" == "1" ]]; then composer check-complete; fi + + # Run the unit tests. + - if [[ $PHPCS_VERSION != "n/a" ]]; then composer run-tests; fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2063917 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Change Log for the PHPCSDevTools standard for PHP Codesniffer + +All notable changes to this project will be documented in this file. + +This projects adheres to [Keep a CHANGELOG](http://keepachangelog.com/) and uses [Semantic Versioning](http://semver.org/). + + +## [Unreleased] + +_Nothing yet._ + + +## 1.0.0 - 2020-02-12 + +Initial release containing: +* Feature completeness checking tool for PHPCS sniffs. +* A `PHPCSDebug` standard to help debugging sniffs. + + +[Unreleased]: https://github.com/PHPCSStandards/PHPCSDevTools/compare/1.0.0...HEAD + diff --git a/PHPCSDebug/Docs/Debug/TokenListStandard.xml b/PHPCSDebug/Docs/Debug/TokenListStandard.xml new file mode 100644 index 0000000..ae863f4 --- /dev/null +++ b/PHPCSDebug/Docs/Debug/TokenListStandard.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/PHPCSDebug/Sniffs/Debug/TokenListSniff.php b/PHPCSDebug/Sniffs/Debug/TokenListSniff.php new file mode 100644 index 0000000..635ac45 --- /dev/null +++ b/PHPCSDebug/Sniffs/Debug/TokenListSniff.php @@ -0,0 +1,131 @@ + '?', + 'code' => '?', + 'content' => '', + 'line' => '?', + 'column' => '?', + 'level' => 0, + 'conditions' => [], + ]; + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() + { + return [ + \T_OPEN_TAG, + \T_OPEN_TAG_WITH_ECHO, + ]; + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current + * token in the stack. + * + * @return void + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $last = ($phpcsFile->numTokens - 1); + + $ptrPadding = \max(3, \strlen($last)); + $linePadding = \strlen($tokens[$last]['line']); + + echo \PHP_EOL; + echo \str_pad('Ptr', $ptrPadding, ' ', \STR_PAD_BOTH), + ' :: ', \str_pad('Ln', ($linePadding + 1), ' ', \STR_PAD_BOTH), + ' :: ', \str_pad('Col', 4, ' ', \STR_PAD_BOTH), + ' :: ', 'Cond', + ' :: ', \str_pad('Token Type', 26), // Longest token type name is 26 chars. + ' :: [len]: Content', \PHP_EOL; + + echo \str_repeat('-', ($ptrPadding + $linePadding + 35 + 16 + 18)), \PHP_EOL; + + foreach ($tokens as $ptr => $token) { + $token += $this->tokenDefaults; + $content = $token['content']; + + if (isset($token['length']) === false) { + $token['length'] = 0; + if (isset($token['content'])) { + $token['length'] = \strlen($content); + } + } + + if ($token['code'] === \T_WHITESPACE + || (\defined('T_DOC_COMMENT_WHITESPACE') + && $token['code'] === \T_DOC_COMMENT_WHITESPACE) + ) { + if (\strpos($content, "\t") !== false) { + $content = \str_replace("\t", '\t', $content); + } + if (isset($token['orig_content'])) { + $content .= ' :: Orig: ' . \str_replace("\t", '\t', $token['orig_content']); + } + } + + $conditionCount = \count($token['conditions']); + + echo \str_pad($ptr, $ptrPadding, ' ', \STR_PAD_LEFT), + ' :: L', \str_pad($token['line'], $linePadding, '0', \STR_PAD_LEFT), + ' :: C', \str_pad($token['column'], 3, ' ', \STR_PAD_LEFT), + ' :: CC', \str_pad($conditionCount, 2, ' ', \STR_PAD_LEFT), + ' :: ', \str_pad($token['type'], 26), // Longest token type name is 26 chars. + ' :: [', $token['length'], ']: ', $content, \PHP_EOL; + } + + // Only do this once per file. + return ($phpcsFile->numTokens + 1); + } +} diff --git a/PHPCSDebug/Tests/Debug/TokenListUnitTest.inc b/PHPCSDebug/Tests/Debug/TokenListUnitTest.inc new file mode 100644 index 0000000..bfd207a --- /dev/null +++ b/PHPCSDebug/Tests/Debug/TokenListUnitTest.inc @@ -0,0 +1,3 @@ + => + */ + public function getErrorList() + { + return []; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} diff --git a/PHPCSDebug/Tests/Debug/TokenListZUnitTest.php b/PHPCSDebug/Tests/Debug/TokenListZUnitTest.php new file mode 100644 index 0000000..8128955 --- /dev/null +++ b/PHPCSDebug/Tests/Debug/TokenListZUnitTest.php @@ -0,0 +1,46 @@ +assertNotEmpty($output); + + $expected = "\n"; + $expected .= 'Ptr :: Ln :: Col :: Cond :: Token Type :: [len]: Content' . "\n"; + $expected .= '-------------------------------------------------------------------------' . "\n"; + $expected .= ' 0 :: L1 :: C 1 :: CC 0 :: T_OPEN_TAG :: [5]: assertSame($expected, $output); + } +} diff --git a/PHPCSDebug/ruleset.xml b/PHPCSDebug/ruleset.xml new file mode 100644 index 0000000..6afd46f --- /dev/null +++ b/PHPCSDebug/ruleset.xml @@ -0,0 +1,6 @@ + + + + Sniffs which can be used as debugging tools for PHPCS developers + + diff --git a/README.md b/README.md index 00d7b65..c74d118 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,181 @@ -# PHPCSDevTools -Tools for PHP_CodeSniffer sniff developers +PHPCSDevTools for developers of PHP_CodeSniffer sniffs +===================================================== + +[![Latest Stable Version](https://poser.pugx.org/phpcsstandards/phpcsdevtools/v/stable)](https://packagist.org/packages/phpcsstandards/phpcsdevtools) +[![Travis Build Status](https://travis-ci.com/PHPCSStandards/PHPCSDevTools.svg?branch=master)](https://travis-ci.com/PHPCSStandards/PHPCSDevTools/branches) +[![Release Date of the Latest Version](https://img.shields.io/github/release-date/PHPCSStandards/PHPCSDevTools.svg?maxAge=1800)](https://github.com/PHPCSStandards/PHPCSDevTools/releases) +:construction: +[![Latest Unstable Version](https://img.shields.io/badge/unstable-dev--develop-e68718.svg?maxAge=2419200)](https://packagist.org/packages/phpcsstandards/phpcsdevtools#dev-develop) +[![Travis Build Status](https://travis-ci.com/PHPCSStandards/PHPCSDevTools.svg?branch=develop)](https://travis-ci.com/PHPCSStandards/PHPCSDevTools/branches) +[![Last Commit to Unstable](https://img.shields.io/github/last-commit/PHPCSStandards/PHPCSDevTools/develop.svg)](https://github.com/PHPCSStandards/PHPCSDevTools/commits/develop) + +[![Minimum PHP Version](https://img.shields.io/packagist/php-v/phpcsstandards/phpcsdevtools.svg?maxAge=3600)](https://packagist.org/packages/phpcsstandards/phpcsdevtools) +[![Tested on PHP 5.4 to nightly](https://img.shields.io/badge/tested%20on-PHP%205.4%20|%205.5%20|%205.6%20|%207.0%20|%207.1%20|%207.2%20|%207.3%20|%207.4%20|%20nightly-brightgreen.svg?maxAge=2419200)](https://travis-ci.com/PHPCSStandards/PHPCSDevTools) + +[![License: LGPLv3](https://poser.pugx.org/phpcsstandards/phpcsdevtools/license)](https://github.com/PHPCSStandards/PHPCSDevTools/blob/master/LICENSE) +![Awesome](https://img.shields.io/badge/awesome%3F-yes!-brightgreen.svg) + + +This is a set of tools to aid developers of sniffs for [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer). + +* [Installation](#installation) + + [Composer Project-based Installation](#composer-project-based-installation) + + [Composer Global Installation](#composer-global-installation) + + [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) + + [Sniff Debugging](#sniff-debugging) +* [Contributing](#contributing) +* [License](#license) + + +Installation +------------------------------------------- + +### Composer Project-based Installation + +Run the following from the root of your project: +```bash +composer require --dev phpcsstandards/phpcsdevtools:^1.0 +``` + +### Composer Global Installation + +If you work on several different sniff repos, you may want to install this toolset globally: +```bash +composer global require phpcsstandards/phpcsdevtools:^1.0 +``` + +Composer will automatically install dependencies and register the PHPCSDebug standard with PHP_CodeSniffer using the [DealerDirect Composer PHPCS plugin](https://github.com/Dealerdirect/phpcodesniffer-composer-installer/). + + +### Stand-alone Installation + +* Install [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) via [your preferred method](https://github.com/squizlabs/PHP_CodeSniffer#installation). +* Register the path to PHPCS in your system `$PATH` environment variable to make the `phpcs` command available from anywhere in your file system. +* Download the [latest PHPCSDevTools release](https://github.com/PHPCSStandards/PHPCSDevTools/releases) and unzip/untar it into an arbitrary directory. + You can also choose to clone this repository using git. +* Add the path to the directory in which you placed your copy of the PHPCSDevTools repo to the PHP CodeSniffer configuration using the below command: + ```bash + phpcs --config-set installed_paths /path/to/PHPCSDevTools + ``` + **Warning**: :warning: The `installed_paths` command overwrites any previously set `installed_paths`. If you have previously set `installed_paths` for other external standards, run `phpcs --config-show` first and then run the `installed_paths` command with all the paths you need separated by comma's, i.e.: + ```bash + phpcs --config-set installed_paths /path/1,/path/2,/path/3 + ``` + + +Features +------------------------------ + +### Checking whether all sniffs in a PHPCS standard are feature complete + +You can now easily check whether each and every sniff in your standard is accompanied by a documentation XML file (warning) as well as unit test files (error). + +To use the tool, run it from the root of the your standards repo like so: +```bash +# When installed as a project dependency: +vendor/bin/phpcs-check-feature-completeness + +# When installed globally with Composer: +phpcs-check-feature-completeness + +# When installed as a git clone or otherwise: +php -f "path/to/PHPCSDevTools/bin/phpcs-check-feature-completeness" +``` + +If all is good, you will see a `All # sniffs are accompanied by unit tests and documentation.` message. + +If there are files missing, you will see errors/warnings for each missing file, like so: +``` +WARNING: Documentation missing for path/to/project/StandardName/Sniffs/Category/SniffNameSniff.php. +ERROR: Unit tests missing for path/to/project/StandardName/Sniffs/Category/SniffNameSniff.php. +``` + +For the fastest results, it is recommended to pass the name of the subdirectory where your standard is located to the script, like so: +```bash +phpcs-check-feature-completeness ./StandardName +``` + +#### Options +``` +directories One or more specific directories to examine. + Defaults to the directory from which the script is run. +-q, --quiet Turn off warnings for missing documentation. +--exclude Comma-delimited list of (relative) directories to exclude + from the scan. + Defaults to excluding the /vendor/ directory. +--no-progress Disable progress in console output. +--colors Enable colors in console output. + (disables auto detection of color support) +--no-colors Disable colors in console output. +-v Verbose mode. +-h, --help Print this help. +-V, --version Display the current version of this script. +``` + + +### Sniff Debugging + +Once this project is installed, you will see a new `PHPCSDebug` ruleset in the list of installed standards when you run `phpcs -i`. + +For now, this standard only contains one sniff: `PHPCSDebug.Debug.TokenList`. +This sniff will display compact, but detailed information about the tokens found in a (test case) file. + +This sniff is compatible with PHPCS 3.0+. + +Typical usage: +* Set up a test case file for a new sniff you intend to write. +* Run PHPCS over the test case file using this standard to see a list of the tokens found in the file: +```bash +phpcs ./SniffNameUnitTest.inc --standard=PHPCSDebug +``` +* Or use it together with the new sniff you are developing: +```bash +phpcs ./SniffNameUnitTest.inc --standard=YourStandard,PHPCSDebug --sniffs=YourStandard.Category.NewSniffName,PHPCSDebug.Debug.TokenList +``` + +The output will look something along the lines of: +``` +Ptr :: Ln :: Col :: Cond :: Token Type :: [len]: Content +------------------------------------------------------------------------- + 0 :: L1 :: C 1 :: CC 0 :: T_OPEN_TAG :: [5]: '/Docs/', + 'Sniff.php' => 'Standard.xml', + ]; + + /** + * Search & replace values to convert a sniff file path into a unit test file path. + * + * Keys are the strings to search for, values the replacement values. + * + * @var array + */ + private $sniffToUnitTest = [ + '/Sniffs/' => '/Tests/', + 'Sniff.' => 'UnitTest.', + ]; + + /** + * Possible test case file extensions. + * + * @var array + */ + private $testCaseExtensions = [ + '.inc', + '.css', + '.js', + '.1.inc', + '.1.css', + '.1.js', + ]; + + /** + * Constructor. + */ + public function __construct() + { + $this->processCliCommand(); + + $sep = '/'; + if (empty($this->targetDirs)) { + // If the user didn't provide a path, use the directory from which the script was run. + $this->targetDirs[] = $this->projectRoot; + } else { + // Handle Windows vs Unix file paths. + $sep = \DIRECTORY_SEPARATOR; + } + + // Handle excluded dirs. + $exclude = '(?!\.git/)'; + if (empty($this->excludedDirs) === false) { + $excludedDirs = \array_map( + 'preg_quote', + $this->excludedDirs, + \array_fill(0, \count($this->excludedDirs), '`') + ); + $exclude = '(?!(\.git|' . \implode('|', $excludedDirs) . ')/)'; + } + + // Prepare the regexes. + $quotedProjectRoot = \preg_quote($this->projectRoot . $sep, '`'); + $allFilesRegex = \str_replace('(?!\.git/)', $exclude, FileList::BASE_REGEX); + $allFilesRegex = \sprintf($allFilesRegex, $quotedProjectRoot); + $sniffsRegex = \sprintf(self::FILTER_REGEX, $quotedProjectRoot, $exclude); + + // Get the file lists. + $allFiles = []; + $allSniffs = []; + foreach ($this->targetDirs as $targetDir) { + // Get a list of all files in the target directory. + $allFiles[] = (new FileList($targetDir, $this->projectRoot, $allFilesRegex))->getList(); + + // Get a list of all sniffs in the target directory. + $allSniffs[] = (new FileList($targetDir, $this->projectRoot, $sniffsRegex))->getList(); + } + + $allFiles = \call_user_func_array('array_merge', $allFiles); + \sort($allFiles, \SORT_NATURAL); + $this->allFiles = \array_flip($allFiles); + + $allSniffs = \call_user_func_array('array_merge', $allSniffs); + \sort($allSniffs, \SORT_NATURAL); + $this->allSniffs = $allSniffs; + } + + /** + * Process the received command arguments. + * + * @return void + */ + protected function processCliCommand() + { + $args = $_SERVER['argv']; + + // Remove the call to the script itself. + \array_shift($args); + + $this->projectRoot = \getcwd(); + + if (empty($args)) { + // No options set. + $this->showColored = $this->isColorSupported(); + + return; + } + + $argsFlipped = \array_flip($args); + + if (isset($argsFlipped['-h']) + || isset($argsFlipped['--help']) + ) { + $this->showHelp(); + exit(0); + } + + if (isset($argsFlipped['-V']) + || isset($argsFlipped['--version']) + ) { + $this->showVersion(); + exit(0); + } + + if (isset($argsFlipped['-q']) + || isset($argsFlipped['--quiet']) + ) { + $this->quietMode = true; + } + + if (isset($argsFlipped['--no-progress'])) { + $this->showProgress = false; + } + + if (isset($argsFlipped['--no-colors'])) { + $this->showColored = false; + } elseif (isset($argsFlipped['--colors'])) { + $this->showColored = true; + } else { + $this->showColored = $this->isColorSupported(); + } + + if (isset($argsFlipped['-v'])) { + $this->verbose = 1; + } + + foreach ($args as $arg) { + if (\strpos($arg, '--exclude=') === 0) { + $exclude = \substr($arg, 10); + if ($exclude === '') { + $this->excludedDirs = []; + continue; + } + + $exclude = \explode(',', $exclude); + $exclude = \array_map( + function ($subdir) { + return \trim($subdir, '/'); + }, + $exclude + ); + + $this->excludedDirs = $exclude; + continue; + } + + if ($arg[0] !== '-') { + // The user must have set a path to search. + $realpath = \realpath($arg); + + if ($realpath !== false) { + $this->targetDirs[] = $realpath; + } + } + } + } + + /** + * Validate the completeness of the sniffs in the repository. + * + * @return void + */ + public function validate() + { + $this->showVersion(); + + if ($this->verbose > 0) { + echo 'Target dir(s):', \PHP_EOL, + '- ' . \implode(\PHP_EOL . '- ', $this->targetDirs), + \PHP_EOL, \PHP_EOL; + } + + if ($this->isComplete() !== true) { + exit(1); + } + + exit(0); + } + + /** + * Verify if all files needed for a sniff to be considered complete are available. + * + * @return void + */ + public function isComplete() + { + $sniffCount = \count($this->allSniffs); + if ($sniffCount === 0) { + echo 'No sniffs found.', \PHP_EOL; + return true; + } + + $docWarning = 'WARNING: Documentation missing for %s.'; + $testError = 'ERROR: Unit tests missing for %s.'; + $testCaseError = 'ERROR: Unit test case file missing for %s.'; + + if ($this->showColored === true) { + $docWarning = \str_replace('WARNING', "\033[33mWARNING\033[0m", $docWarning); + $testError = \str_replace('ERROR', "\033[31mERROR\033[0m", $testError); + $testCaseError = \str_replace('ERROR', "\033[31mERROR\033[0m", $testError); + } + + $notices = []; + $warningCount = 0; + $errorCount = 0; + foreach ($this->allSniffs as $i => $file) { + if ($this->quietMode === false) { + $docFile = \str_replace(\array_keys($this->sniffToDoc), $this->sniffToDoc, $file); + if (isset($this->allFiles[$docFile]) === false) { + $notices[] = \sprintf($docWarning, $file); + ++$warningCount; + } + } + + $testFile = \str_replace(\array_keys($this->sniffToUnitTest), $this->sniffToUnitTest, $file); + if (isset($this->allFiles[$testFile]) === false) { + $notices[] = \sprintf($testError, $file); + ++$errorCount; + } else { + $fileFound = false; + foreach ($this->testCaseExtensions as $extension) { + $testCaseFile = \str_replace('.php', $extension, $testFile); + if (isset($this->allFiles[$testCaseFile]) === true) { + $fileFound = true; + break; + } + } + + if ($fileFound === false) { + $notices[] = \sprintf($testCaseError, $file); + ++$errorCount; + } + } + + // Show progress. + if ($this->showProgress === true) { + echo '.'; + + $current = ($i + 1); + if (($current % 60) === 0 || $current === $sniffCount) { + $padding = \strlen($sniffCount); + + $filling = ''; + if ($current === $sniffCount) { + $lines = \ceil($current / 60); + if ($lines > 1) { + $filling = \str_repeat(' ', (($lines * 60) - $sniffCount)); + } + } + + echo $filling, ' ', \str_pad($current, $padding, ' ', \STR_PAD_LEFT), ' / ', $sniffCount, + ' (', \str_pad(\round(($current / $sniffCount) * 100), 3, ' ', \STR_PAD_LEFT), '%)', \PHP_EOL; + } + } + } + + /* + * Show feedback to the user. + */ + if (empty($notices) === false) { + // Show the errors and warnings. + echo \PHP_EOL, + \implode(\PHP_EOL, $notices), \PHP_EOL, + \PHP_EOL, + '-----------------------------------------', \PHP_EOL, + \sprintf('Found %d errors and %d warnings', $errorCount, $warningCount), \PHP_EOL; + + return false; + } else { + $feedback = "All $sniffCount sniffs are"; + if ($sniffCount === 1) { + $feedback = "Found $sniffCount sniff"; + } + + if ($this->quietMode === false) { + $feedback .= ' accompanied by unit tests and documentation.'; + } else { + $feedback .= ' accompanied by unit tests.'; + } + + if ($this->showColored === true) { + $feedback = "\033[32m" . $feedback . "\033[0m"; + } + + echo \PHP_EOL, \PHP_EOL, $feedback, \PHP_EOL; + + return true; + } + } + + /** + * Detect whether or not the CLI supports colored output. + * + * @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; + } + + // Linux/MacOS. + if (\function_exists('posix_isatty')) { + return @\posix_isatty(\STDOUT); + } + + return false; + } + + /** + * Display the version number of this script. + * + * @return void + */ + protected function showVersion() + { + echo 'PHPCSDevTools: Sniff feature completeness checker version '; + include __DIR__ . '/../VERSION'; + echo \PHP_EOL, + 'by Juliette Reinders Folmer', \PHP_EOL, \PHP_EOL; + } + + /** + * Display usage instructions. + * + * @return void + */ + protected function showHelp() + { + $this->showVersion(); + + echo 'Usage:', \PHP_EOL, + ' phpcs-check-feature-completeness', \PHP_EOL, + ' phpcs-check-feature-completeness [-q] [--exclude=] [directories]', \PHP_EOL; + + echo \PHP_EOL, + 'Options:', \PHP_EOL, + ' directories One or more specific directories to examine.', \PHP_EOL, + ' Defaults to the directory from which the script is run.', \PHP_EOL, + ' -q, --quiet Turn off warnings for missing documentation.', \PHP_EOL, + ' --exclude Comma-delimited list of (relative) directories to exclude', \PHP_EOL, + ' from the scan.', \PHP_EOL, + ' Defaults to excluding the /vendor/ directory.', \PHP_EOL, + ' --no-progress Disable progress in console output.', \PHP_EOL, + ' --colors Enable colors in console output.', \PHP_EOL, + ' (disables auto detection of color support)', \PHP_EOL, + ' --no-colors Disable colors in console output.', \PHP_EOL, + ' -v Verbose mode.', \PHP_EOL, + ' -h, --help Print this help.', \PHP_EOL, + ' -V, --version Display the current version of this script.', \PHP_EOL; + } +} diff --git a/Scripts/FileList.php b/Scripts/FileList.php new file mode 100644 index 0000000..bc9aff9 --- /dev/null +++ b/Scripts/FileList.php @@ -0,0 +1,110 @@ +rootPath = $rootPath; + + $directory = new RecursiveDirectoryIterator( + $directory, + FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS + ); + + $flattened = new RecursiveIteratorIterator( + $directory, + RecursiveIteratorIterator::LEAVES_ONLY, + RecursiveIteratorIterator::CATCH_GET_CHILD + ); + + if ($filter === '') { + $filter = \sprintf(self::BASE_REGEX, \preg_quote($this->rootPath . \DIRECTORY_SEPARATOR, '`')); + } + + $this->fileIterator = new RegexIterator($flattened, $filter); + + return $this; + } + + /** + * Retrieve the filtered file list iterator. + * + * @return array + */ + public function getIterator() + { + return $this->fileIterator; + } + + /** + * Retrieve the filtered file list as an array. + * + * @return array + */ + public function getList() + { + $fileList = []; + + foreach ($this->fileIterator as $file) { + $fileList[] = \str_replace($this->rootPath, '', $file); + } + + return $fileList; + } +} diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..afaf360 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/bin/phpcs-check-feature-completeness b/bin/phpcs-check-feature-completeness new file mode 100644 index 0000000..fc0cb22 --- /dev/null +++ b/bin/phpcs-check-feature-completeness @@ -0,0 +1,44 @@ +#!/usr/bin/env php +] [directories] + * + * Options: + * directories One or more specific directories to examine. + * Defaults to the directory from which the script is run. + * -q, --quiet Turn off warnings for missing documentation. + * --exclude Comma-delimited list of (relative) directories to exclude + * from the scan. + * Defaults to excluding the /vendor/ directory. + * --no-progress Disable progress in console output. + * --colors Enable colors in console output. + * (disables auto detection of color support) + * --no-colors Disable colors in console output. + * -v Verbose mode. + * -h, --help Print this help. + * -V, --version Display the current version of this script. + * + * @package PHPCSDevTools + * @copyright 2019 PHPCSDevTools Contributors + * @license https://opensource.org/licenses/LGPL-3.0 LGPL3 + * @link https://github.com/PHPCSStandards/PHPCSDevTools + */ + +if (is_file(__DIR__.'/../autoload.php') === true) { + // Installed via Composer. + require_once __DIR__.'/../autoload.php'; +} else { + // Presume git clone. + require_once __DIR__ . '/../Scripts/FileList.php'; + require_once __DIR__ . '/../Scripts/CheckSniffCompleteness.php'; +} + +$validate = new PHPCSDevTools\Scripts\CheckSniffCompleteness(); +$validate->validate(); diff --git a/composer.json b/composer.json index ee18941..39380f6 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name" : "phpcsstandards/phpcsdevtools", "description" : "Tools for PHP_CodeSniffer sniff developers.", "type" : "phpcodesniffer-standard", - "keywords" : [ "phpcs", "devtools", "php_codesniffer" ], + "keywords" : [ "phpcs", "devtools", "debug", "php_codesniffer", "phpcodesniffer-standard" ], "license" : "LGPL-3.0-or-later", "authors" : [ { @@ -22,16 +22,33 @@ "require" : { "php" : ">=5.4", "squizlabs/php_codesniffer" : "^3.0.2", - "dealerdirect/phpcodesniffer-composer-installer" : "^0.5" + "dealerdirect/phpcodesniffer-composer-installer" : "^0.3 || ^0.4.1 || ^0.5 || ^0.6.2" }, "require-dev" : { "roave/security-advisories" : "dev-master", + "phpunit/phpunit" : "^4.5 || ^5.0 || ^6.0 || ^7.0", "jakub-onderka/php-parallel-lint": "^1.0", - "jakub-onderka/php-console-highlighter": "^0.4" + "jakub-onderka/php-console-highlighter": "^0.4", + "phpcsstandards/phpcsdevcs": "^1.0.0" }, + "bin": [ + "bin/phpcs-check-feature-completeness" + ], "scripts" : { "lint": [ - "@php ./vendor/jakub-onderka/php-parallel-lint/parallel-lint . -e php --exclude vendor" + "@php ./vendor/jakub-onderka/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" + ], + "check-cs": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" + ], + "fix-cs": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf" + ], + "run-tests": [ + "@php ./vendor/phpunit/phpunit/phpunit --filter PHPCSDebug ./vendor/squizlabs/php_codesniffer/tests/AllTests.php" + ], + "check-complete": [ + "@php ./bin/phpcs-check-feature-completeness ./PHPCSDebug" ] } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..6d60988 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,62 @@ + + + Check the code of the PHPCSDevTools standard itself. + + + + . + + + */vendor/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /phpunit-bootstrap\.php$ + + + diff --git a/phpunit-bootstrap.php b/phpunit-bootstrap.php new file mode 100644 index 0000000..b15395e --- /dev/null +++ b/phpunit-bootstrap.php @@ -0,0 +1,83 @@ + true, +]; + +$allStandards = PHP_CodeSniffer\Util\Standards::getInstalledStandards(); +$allStandards[] = 'Generic'; + +$standardsToIgnore = []; +foreach ($allStandards as $standard) { + if (isset($phpcsDevtoolsStandards[$standard]) === true) { + continue; + } + + $standardsToIgnore[] = $standard; +} + +$standardsToIgnoreString = \implode(',', $standardsToIgnore); +\putenv("PHPCS_IGNORE_TESTS={$standardsToIgnoreString}"); + +// Clean up. +unset($ds, $phpcsDir, $composerPHPCSPath); +unset($ds, $phpcsDir, $composerPHPCSPath, $allStandards, $standardsToIgnore, $standard, $standardsToIgnoreString); diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..c6d81a1 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,16 @@ + + + + + + ./PHPCSDebug/Tests/ + + +