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/
+
+
+