resolved conflicts

This commit is contained in:
2025-03-31 03:46:05 +03:00
6454 changed files with 1520539 additions and 9 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,153 @@
# PHP_CodeSniffer
<div aria-hidden="true">
[![Latest Stable Version](https://img.shields.io/packagist/v/squizlabs/php_codesniffer?label=Stable)](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases)
[![Validate](https://github.com/PHPCSStandards/PHP_CodeSniffer/actions/workflows/validate.yml/badge.svg?branch=master)](https://github.com/PHPCSStandards/PHP_CodeSniffer/actions/workflows/validate.yml)
[![Test](https://github.com/PHPCSStandards/PHP_CodeSniffer/actions/workflows/test.yml/badge.svg?branch=master)][GHA-test]
[![Coverage Status](https://coveralls.io/repos/github/PHPCSStandards/PHP_CodeSniffer/badge.svg?branch=master)](https://coveralls.io/github/PHPCSStandards/PHP_CodeSniffer?branch=master)
[![License](https://img.shields.io/github/license/PHPCSStandards/PHP_CodeSniffer)](https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt)
![Minimum PHP Version](https://img.shields.io/packagist/dependency-v/squizlabs/php_codesniffer/php.svg)
[![Tested on PHP 5.4 to 8.4](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|%208.0%20|%208.1%20|%208.2%20|%208.3%20|%208.4-brightgreen.svg?maxAge=2419200)][GHA-test]
[GHA-test]: https://github.com/PHPCSStandards/PHP_CodeSniffer/actions/workflows/test.yml
</div>
> [!NOTE]
> This package is the official continuation of the now abandoned [PHP_CodeSniffer package which was created by Squizlabs](https://github.com/squizlabs/PHP_CodeSniffer).
## About
PHP_CodeSniffer is a set of two PHP scripts; the main `phpcs` script that tokenizes PHP, JavaScript and CSS files to detect violations of a defined coding standard, and a second `phpcbf` script to automatically correct coding standard violations. PHP_CodeSniffer is an essential development tool that ensures your code remains clean and consistent.
## Requirements
PHP_CodeSniffer requires PHP version 5.4.0 or greater, although individual sniffs may have additional requirements such as external applications and scripts. See the [Configuration Options manual page](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Configuration-Options) for a list of these requirements.
If you're using PHP_CodeSniffer as part of a team, or you're running it on a [CI](https://en.wikipedia.org/wiki/Continuous_integration) server, you may want to configure your project's settings [using a configuration file](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Advanced-Usage#using-a-default-configuration-file).
## Installation
The easiest way to get started with PHP_CodeSniffer is to download the Phar files for each of the commands:
```bash
# Download using curl
curl -OL https://phars.phpcodesniffer.com/phpcs.phar
curl -OL https://phars.phpcodesniffer.com/phpcbf.phar
# Or download using wget
wget https://phars.phpcodesniffer.com/phpcs.phar
wget https://phars.phpcodesniffer.com/phpcbf.phar
# Then test the downloaded PHARs
php phpcs.phar -h
php phpcbf.phar -h
```
These Phars are signed with the official Release key for PHPCS with the
fingerprint `689D AD77 8FF0 8760 E046 228B A978 2203 05CD 5C32`.
As of PHP_CodeSniffer 3.10.3, the provenance of PHAR files associated with a release can be verified via [GitHub Artifact Attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds) using the [GitHub CLI tool](https://cli.github.com/) with the following command: `gh attestation verify [phpcs|phpcbf].phar -o PHPCSStandards`.
### Composer
If you use Composer, you can install PHP_CodeSniffer system-wide with the following command:
```bash
composer global require "squizlabs/php_codesniffer=*"
```
Make sure you have the composer bin dir in your PATH. The default value is `~/.composer/vendor/bin/`, but you can check the value that you need to use by running `composer global config bin-dir --absolute`.
Or alternatively, include a dependency for `squizlabs/php_codesniffer` in your `composer.json` file. For example:
```json
{
"require-dev": {
"squizlabs/php_codesniffer": "^3.0"
}
}
```
You will then be able to run PHP_CodeSniffer from the vendor bin directory:
```bash
./vendor/bin/phpcs -h
./vendor/bin/phpcbf -h
```
### Phive
If you use Phive, you can install PHP_CodeSniffer as a project tool using the following commands:
```bash
phive install --trust-gpg-keys 689DAD778FF08760E046228BA978220305CD5C32 phpcs
phive install --trust-gpg-keys 689DAD778FF08760E046228BA978220305CD5C32 phpcbf
```
You will then be able to run PHP_CodeSniffer from the `tools` directory:
```bash
./tools/phpcs -h
./tools/phpcbf -h
```
### Git Clone
You can also download the PHP_CodeSniffer source and run the `phpcs` and `phpcbf` commands directly from the Git clone:
```bash
git clone https://github.com/PHPCSStandards/PHP_CodeSniffer.git
cd PHP_CodeSniffer
php bin/phpcs -h
php bin/phpcbf -h
```
## Getting Started
The default coding standard used by PHP_CodeSniffer is the PEAR coding standard. To check a file against the PEAR coding standard, simply specify the file's location:
```bash
phpcs /path/to/code/myfile.php
```
Or if you wish to check an entire directory you can specify the directory location instead of a file.
```bash
phpcs /path/to/code-directory
```
If you wish to check your code against the PSR-12 coding standard, use the `--standard` command line argument:
```bash
phpcs --standard=PSR12 /path/to/code-directory
```
If PHP_CodeSniffer finds any coding standard errors, a report will be shown after running the command.
Full usage information and example reports are available on the [usage page](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Usage).
## Documentation
The documentation for PHP_CodeSniffer is available on the [GitHub wiki](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki).
## Issues
Bug reports and feature requests can be submitted on the [GitHub Issue Tracker](https://github.com/PHPCSStandards/PHP_CodeSniffer/issues).
## Contributing
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for information.
## Versioning
PHP_CodeSniffer uses a `MAJOR.MINOR.PATCH` version number format.
The `MAJOR` version is incremented when:
- backwards-incompatible changes are made to how the `phpcs` or `phpcbf` commands are used, or
- backwards-incompatible changes are made to the `ruleset.xml` format, or
- backwards-incompatible changes are made to the API used by sniff developers, or
- custom PHP_CodeSniffer token types are removed, or
- existing sniffs are removed from PHP_CodeSniffer entirely
The `MINOR` version is incremented when:
- new backwards-compatible features are added to the `phpcs` and `phpcbf` commands, or
- backwards-compatible changes are made to the `ruleset.xml` format, or
- backwards-compatible changes are made to the API used by sniff developers, or
- new sniffs are added to an included standard, or
- existing sniffs are removed from an included standard
> NOTE: Backwards-compatible changes to the API used by sniff developers will allow an existing sniff to continue running without producing fatal errors but may not result in the sniff reporting the same errors as it did previously without changes being required.
The `PATCH` version is incremented when:
- backwards-compatible bug fixes are made
> NOTE: As PHP_CodeSniffer exists to report and fix issues, most bugs are the result of coding standard errors being incorrectly reported or coding standard errors not being reported when they should be. This means that the messages produced by PHP_CodeSniffer, and the fixes it makes, are likely to be different between PATCH versions.
@@ -0,0 +1,345 @@
<?php
/**
* Autoloads files for PHP_CodeSniffer and tracks what has been loaded.
*
* Due to different namespaces being used for custom coding standards,
* the autoloader keeps track of what class is loaded after a file is included,
* even if the file is ultimately included by another autoloader (such as composer).
*
* This allows PHP_CodeSniffer to request the class name after loading a class
* when it only knows the filename, without having to parse the file to find it.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer;
use Composer\Autoload\ClassLoader;
use Exception;
if (class_exists('PHP_CodeSniffer\Autoload', false) === false) {
class Autoload
{
/**
* The composer autoloader.
*
* @var \Composer\Autoload\ClassLoader
*/
private static $composerAutoloader = null;
/**
* A mapping of file names to class names.
*
* @var array<string, string>
*/
private static $loadedClasses = [];
/**
* A mapping of class names to file names.
*
* @var array<string, string>
*/
private static $loadedFiles = [];
/**
* A list of additional directories to search during autoloading.
*
* This is typically a list of coding standard directories.
*
* @var string[]
*/
private static $searchPaths = [];
/**
* Loads a class.
*
* This method only loads classes that exist in the PHP_CodeSniffer namespace.
* All other classes are ignored and loaded by subsequent autoloaders.
*
* @param string $class The name of the class to load.
*
* @return bool
*/
public static function load($class)
{
// Include the composer autoloader if there is one, but re-register it
// so this autoloader runs before the composer one as we need to include
// all files so we can figure out what the class/interface/trait name is.
if (self::$composerAutoloader === null) {
// Make sure we don't try to load any of Composer's classes
// while the autoloader is being setup.
if (strpos($class, 'Composer\\') === 0) {
return false;
}
if (strpos(__DIR__, 'phar://') !== 0
&& @file_exists(__DIR__.'/../../autoload.php') === true
) {
self::$composerAutoloader = include __DIR__.'/../../autoload.php';
if (self::$composerAutoloader instanceof ClassLoader) {
self::$composerAutoloader->unregister();
self::$composerAutoloader->register();
} else {
// Something went wrong, so keep going without the autoloader
// although namespaced sniffs might error.
self::$composerAutoloader = false;
}
} else {
self::$composerAutoloader = false;
}
}//end if
$ds = DIRECTORY_SEPARATOR;
$path = false;
if (substr($class, 0, 16) === 'PHP_CodeSniffer\\') {
if (substr($class, 0, 22) === 'PHP_CodeSniffer\Tests\\') {
$isInstalled = !is_dir(__DIR__.$ds.'tests');
if ($isInstalled === false) {
$path = __DIR__.$ds.'tests';
} else {
$path = '@test_dir@'.$ds.'PHP_CodeSniffer'.$ds.'CodeSniffer';
}
$path .= $ds.substr(str_replace('\\', $ds, $class), 22).'.php';
} else {
$path = __DIR__.$ds.'src'.$ds.substr(str_replace('\\', $ds, $class), 16).'.php';
}
}
// See if the composer autoloader knows where the class is.
if ($path === false && self::$composerAutoloader !== false) {
$path = self::$composerAutoloader->findFile($class);
}
// See if the class is inside one of our alternate search paths.
if ($path === false) {
foreach (self::$searchPaths as $searchPath => $nsPrefix) {
$className = $class;
if ($nsPrefix !== '' && substr($class, 0, strlen($nsPrefix)) === $nsPrefix) {
$className = substr($class, (strlen($nsPrefix) + 1));
}
$path = $searchPath.$ds.str_replace('\\', $ds, $className).'.php';
if (is_file($path) === true) {
break;
}
$path = false;
}
}
if ($path !== false && is_file($path) === true) {
self::loadFile($path);
return true;
}
return false;
}//end load()
/**
* Includes a file and tracks what class or interface was loaded as a result.
*
* @param string $path The path of the file to load.
*
* @return string The fully qualified name of the class in the loaded file.
*/
public static function loadFile($path)
{
if (strpos(__DIR__, 'phar://') !== 0) {
$path = realpath($path);
if ($path === false) {
return false;
}
}
if (isset(self::$loadedClasses[$path]) === true) {
return self::$loadedClasses[$path];
}
$classesBeforeLoad = [
'classes' => get_declared_classes(),
'interfaces' => get_declared_interfaces(),
'traits' => get_declared_traits(),
];
include $path;
$classesAfterLoad = [
'classes' => get_declared_classes(),
'interfaces' => get_declared_interfaces(),
'traits' => get_declared_traits(),
];
$className = self::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
self::$loadedClasses[$path] = $className;
self::$loadedFiles[$className] = $path;
return self::$loadedClasses[$path];
}//end loadFile()
/**
* Determine which class was loaded based on the before and after lists of loaded classes.
*
* @param array $classesBeforeLoad The classes/interfaces/traits before the file was included.
* @param array $classesAfterLoad The classes/interfaces/traits after the file was included.
*
* @return string The fully qualified name of the class in the loaded file.
*/
public static function determineLoadedClass($classesBeforeLoad, $classesAfterLoad)
{
$className = null;
$newClasses = array_diff($classesAfterLoad['classes'], $classesBeforeLoad['classes']);
if (PHP_VERSION_ID < 70400) {
$newClasses = array_reverse($newClasses);
}
// Since PHP 7.4 get_declared_classes() does not guarantee any order, making
// it impossible to use order to determine which is the parent and which is the child.
// Let's reduce the list of candidates by removing all the classes known to be "parents".
// That way, at the end, only the "main" class just included will remain.
$newClasses = array_reduce(
$newClasses,
function ($remaining, $current) {
return array_diff($remaining, class_parents($current));
},
$newClasses
);
foreach ($newClasses as $name) {
if (isset(self::$loadedFiles[$name]) === false) {
$className = $name;
break;
}
}
if ($className === null) {
$newClasses = array_reverse(array_diff($classesAfterLoad['interfaces'], $classesBeforeLoad['interfaces']));
foreach ($newClasses as $name) {
if (isset(self::$loadedFiles[$name]) === false) {
$className = $name;
break;
}
}
}
if ($className === null) {
$newClasses = array_reverse(array_diff($classesAfterLoad['traits'], $classesBeforeLoad['traits']));
foreach ($newClasses as $name) {
if (isset(self::$loadedFiles[$name]) === false) {
$className = $name;
break;
}
}
}
return $className;
}//end determineLoadedClass()
/**
* Adds a directory to search during autoloading.
*
* @param string $path The path to the directory to search.
* @param string $nsPrefix The namespace prefix used by files under this path.
*
* @return void
*/
public static function addSearchPath($path, $nsPrefix='')
{
self::$searchPaths[$path] = rtrim(trim((string) $nsPrefix), '\\');
}//end addSearchPath()
/**
* Retrieve the namespaces and paths registered by external standards.
*
* @return array
*/
public static function getSearchPaths()
{
return self::$searchPaths;
}//end getSearchPaths()
/**
* Gets the class name for the given file path.
*
* @param string $path The name of the file.
*
* @throws \Exception If the file path has not been loaded.
* @return string
*/
public static function getLoadedClassName($path)
{
if (isset(self::$loadedClasses[$path]) === false) {
throw new Exception("Cannot get class name for $path; file has not been included");
}
return self::$loadedClasses[$path];
}//end getLoadedClassName()
/**
* Gets the file path for the given class name.
*
* @param string $class The name of the class.
*
* @throws \Exception If the class name has not been loaded.
* @return string
*/
public static function getLoadedFileName($class)
{
if (isset(self::$loadedFiles[$class]) === false) {
throw new Exception("Cannot get file name for $class; class has not been included");
}
return self::$loadedFiles[$class];
}//end getLoadedFileName()
/**
* Gets the mapping of file names to class names.
*
* @return array<string, string>
*/
public static function getLoadedClasses()
{
return self::$loadedClasses;
}//end getLoadedClasses()
/**
* Gets the mapping of class names to file names.
*
* @return array<string, string>
*/
public static function getLoadedFiles()
{
return self::$loadedFiles;
}//end getLoadedFiles()
}//end class
// Register the autoloader before any existing autoloaders to ensure
// it gets a chance to hear about every autoload request, and record
// the file and class name for it.
spl_autoload_register(__NAMESPACE__.'\Autoload::load', true, true);
}//end if
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env php
<?php
/**
* PHP Code Beautifier and Fixer fixes violations of a defined coding standard.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
require_once __DIR__.'/../autoload.php';
$runner = new PHP_CodeSniffer\Runner();
$exitCode = $runner->runPHPCBF();
exit($exitCode);
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env php
<?php
/**
* PHP_CodeSniffer detects violations of a defined coding standard.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
require_once __DIR__.'/../autoload.php';
$runner = new PHP_CodeSniffer\Runner();
$exitCode = $runner->runPHPCS();
exit($exitCode);
@@ -0,0 +1,90 @@
{
"name": "squizlabs/php_codesniffer",
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"license": "BSD-3-Clause",
"type": "library",
"keywords": [
"phpcs",
"standards",
"static analysis"
],
"authors": [
{
"name": "Greg Sherwood",
"role": "Former lead"
},
{
"name": "Juliette Reinders Folmer",
"role": "Current lead"
},
{
"name": "Contributors",
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
}
],
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
"support": {
"issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
"wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki",
"source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
"security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy"
},
"require": {
"php": ">=5.4.0",
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*"
},
"require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
},
"bin": [
"bin/phpcbf",
"bin/phpcs"
],
"config": {
"lock": false
},
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"scripts": {
"cs": [
"@php ./bin/phpcs"
],
"cbf": [
"@php ./bin/phpcbf"
],
"test": [
"Composer\\Config::disableProcessTimeout",
"@php ./vendor/phpunit/phpunit/phpunit tests/AllTests.php --no-coverage"
],
"coverage": [
"Composer\\Config::disableProcessTimeout",
"@php ./vendor/phpunit/phpunit/phpunit tests/AllTests.php -d max_execution_time=0"
],
"coverage-local": [
"Composer\\Config::disableProcessTimeout",
"@php ./vendor/phpunit/phpunit/phpunit tests/AllTests.php --coverage-html ./build/coverage-html -d max_execution_time=0"
],
"build": [
"Composer\\Config::disableProcessTimeout",
"@php -d phar.readonly=0 -f ./scripts/build-phar.php"
],
"check-all": [
"@cs",
"@test"
]
},
"scripts-descriptions": {
"cs": "Check for code style violations.",
"cbf": "Fix code style violations.",
"test": "Run the unit tests without code coverage.",
"coverage": "Run the unit tests with code coverage.",
"coverage-local": "Run the unit tests with code coverage and generate an HTML report in a 'build' directory.",
"build": "Create PHAR files for PHPCS and PHPCBF.",
"check-all": "Run all checks (phpcs, tests)."
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
<?php
/**
* An exception thrown by PHP_CodeSniffer when it wants to exit from somewhere not in the main runner.
*
* Allows the runner to return an exit code instead of putting exit codes elsewhere
* in the source code.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Exceptions;
use Exception;
class DeepExitException extends Exception
{
}//end class
@@ -0,0 +1,17 @@
<?php
/**
* An exception thrown by PHP_CodeSniffer when it encounters an unrecoverable error.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Exceptions;
use RuntimeException as PHPRuntimeException;
class RuntimeException extends PHPRuntimeException
{
}//end class
@@ -0,0 +1,17 @@
<?php
/**
* An exception thrown by PHP_CodeSniffer when it encounters an unrecoverable tokenizer error.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Exceptions;
use Exception;
class TokenizerException extends Exception
{
}//end class
@@ -0,0 +1,82 @@
<?php
/**
* A dummy file represents a chunk of text that does not have a file system location.
*
* Dummy files can also represent a changed (but not saved) version of a file
* and so can have a file path either set manually, or set by putting
* phpcs_input_file: /path/to/file
* as the first line of the file contents.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Files;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Ruleset;
class DummyFile extends File
{
/**
* Creates a DummyFile object and sets the content.
*
* @param string $content The content of the file.
* @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
* @param \PHP_CodeSniffer\Config $config The config data for the run.
*
* @return void
*/
public function __construct($content, Ruleset $ruleset, Config $config)
{
$this->setContent($content);
// See if a filename was defined in the content.
// This is done by including: phpcs_input_file: [file path]
// as the first line of content.
$path = 'STDIN';
if ($content !== '') {
if (substr($content, 0, 17) === 'phpcs_input_file:') {
$eolPos = strpos($content, $this->eolChar);
$filename = trim(substr($content, 17, ($eolPos - 17)));
$content = substr($content, ($eolPos + strlen($this->eolChar)));
$path = $filename;
$this->setContent($content);
}
}
// The CLI arg overrides anything passed in the content.
if ($config->stdinPath !== null) {
$path = $config->stdinPath;
}
parent::__construct($path, $ruleset, $config);
}//end __construct()
/**
* Set the error, warning, and fixable counts for the file.
*
* @param int $errorCount The number of errors found.
* @param int $warningCount The number of warnings found.
* @param int $fixableCount The number of fixable errors found.
* @param int $fixedCount The number of errors that were fixed.
*
* @return void
*/
public function setErrorCounts($errorCount, $warningCount, $fixableCount, $fixedCount)
{
$this->errorCount = $errorCount;
$this->warningCount = $warningCount;
$this->fixableCount = $fixableCount;
$this->fixedCount = $fixedCount;
}//end setErrorCounts()
}//end class
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,261 @@
<?php
/**
* Represents a list of files on the file system that are to be checked during the run.
*
* File objects are created as needed rather than all at once.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Files;
use Countable;
use FilesystemIterator;
use Iterator;
use PHP_CodeSniffer\Autoload;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Exceptions\DeepExitException;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Util\Common;
use RecursiveArrayIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReturnTypeWillChange;
class FileList implements Iterator, Countable
{
/**
* A list of file paths that are included in the list.
*
* @var array
*/
private $files = [];
/**
* The number of files in the list.
*
* @var integer
*/
private $numFiles = 0;
/**
* The config data for the run.
*
* @var \PHP_CodeSniffer\Config
*/
public $config = null;
/**
* The ruleset used for the run.
*
* @var \PHP_CodeSniffer\Ruleset
*/
public $ruleset = null;
/**
* An array of patterns to use for skipping files.
*
* @var array
*/
protected $ignorePatterns = [];
/**
* Constructs a file list and loads in an array of file paths to process.
*
* @param \PHP_CodeSniffer\Config $config The config data for the run.
* @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
*
* @return void
*/
public function __construct(Config $config, Ruleset $ruleset)
{
$this->ruleset = $ruleset;
$this->config = $config;
$paths = $config->files;
foreach ($paths as $path) {
$isPharFile = Common::isPharFile($path);
if (is_dir($path) === true || $isPharFile === true) {
if ($isPharFile === true) {
$path = 'phar://'.$path;
}
$filterClass = $this->getFilterClass();
$di = new RecursiveDirectoryIterator($path, (RecursiveDirectoryIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS));
$filter = new $filterClass($di, $path, $config, $ruleset);
$iterator = new RecursiveIteratorIterator($filter);
foreach ($iterator as $file) {
$this->files[$file->getPathname()] = null;
$this->numFiles++;
}
} else {
$this->addFile($path);
}//end if
}//end foreach
reset($this->files);
}//end __construct()
/**
* Add a file to the list.
*
* If a file object has already been created, it can be passed here.
* If it is left NULL, it will be created when accessed.
*
* @param string $path The path to the file being added.
* @param \PHP_CodeSniffer\Files\File $file The file being added.
*
* @return void
*/
public function addFile($path, $file=null)
{
// No filtering is done for STDIN when the filename
// has not been specified.
if ($path === 'STDIN') {
$this->files[$path] = $file;
$this->numFiles++;
return;
}
$filterClass = $this->getFilterClass();
$di = new RecursiveArrayIterator([$path]);
$filter = new $filterClass($di, $path, $this->config, $this->ruleset);
$iterator = new RecursiveIteratorIterator($filter);
foreach ($iterator as $path) {
$this->files[$path] = $file;
$this->numFiles++;
}
}//end addFile()
/**
* Get the class name of the filter being used for the run.
*
* @return string
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException If the specified filter could not be found.
*/
private function getFilterClass()
{
$filterType = $this->config->filter;
if ($filterType === null) {
$filterClass = '\PHP_CodeSniffer\Filters\Filter';
} else {
if (strpos($filterType, '.') !== false) {
// This is a path to a custom filter class.
$filename = realpath($filterType);
if ($filename === false) {
$error = "ERROR: Custom filter \"$filterType\" not found".PHP_EOL;
throw new DeepExitException($error, 3);
}
$filterClass = Autoload::loadFile($filename);
} else {
$filterClass = '\PHP_CodeSniffer\Filters\\'.$filterType;
}
}
return $filterClass;
}//end getFilterClass()
/**
* Rewind the iterator to the first file.
*
* @return void
*/
#[ReturnTypeWillChange]
public function rewind()
{
reset($this->files);
}//end rewind()
/**
* Get the file that is currently being processed.
*
* @return \PHP_CodeSniffer\Files\File
*/
#[ReturnTypeWillChange]
public function current()
{
$path = key($this->files);
if (isset($this->files[$path]) === false) {
$this->files[$path] = new LocalFile($path, $this->ruleset, $this->config);
}
return $this->files[$path];
}//end current()
/**
* Return the file path of the current file being processed.
*
* @return string|null Path name or `null` when the end of the iterator has been reached.
*/
#[ReturnTypeWillChange]
public function key()
{
return key($this->files);
}//end key()
/**
* Move forward to the next file.
*
* @return void
*/
#[ReturnTypeWillChange]
public function next()
{
next($this->files);
}//end next()
/**
* Checks if current position is valid.
*
* @return boolean
*/
#[ReturnTypeWillChange]
public function valid()
{
if (current($this->files) === false) {
return false;
}
return true;
}//end valid()
/**
* Return the number of files in the list.
*
* @return integer
*/
#[ReturnTypeWillChange]
public function count()
{
return $this->numFiles;
}//end count()
}//end class
@@ -0,0 +1,219 @@
<?php
/**
* A local file represents a chunk of text has a file system location.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Files;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Util\Cache;
use PHP_CodeSniffer\Util\Common;
class LocalFile extends File
{
/**
* Creates a LocalFile object and sets the content.
*
* @param string $path The absolute path to the file.
* @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
* @param \PHP_CodeSniffer\Config $config The config data for the run.
*
* @return void
*/
public function __construct($path, Ruleset $ruleset, Config $config)
{
$this->path = trim($path);
if (Common::isReadable($this->path) === false) {
parent::__construct($this->path, $ruleset, $config);
$error = 'Error opening file; file no longer exists or you do not have access to read the file';
$this->addMessage(true, $error, 1, 1, 'Internal.LocalFile', [], 5, false);
$this->ignored = true;
return;
}
// Before we go and spend time tokenizing this file, just check
// to see if there is a tag up top to indicate that the whole
// file should be ignored. It must be on one of the first two lines.
if ($config->annotations === true) {
$handle = fopen($this->path, 'r');
if ($handle !== false) {
$firstContent = fgets($handle);
$firstContent .= fgets($handle);
fclose($handle);
if (strpos($firstContent, '@codingStandardsIgnoreFile') !== false
|| stripos($firstContent, 'phpcs:ignorefile') !== false
) {
// We are ignoring the whole file.
$this->ignored = true;
return;
}
}
}
$this->reloadContent();
parent::__construct($this->path, $ruleset, $config);
}//end __construct()
/**
* Loads the latest version of the file's content from the file system.
*
* @return void
*/
public function reloadContent()
{
$this->setContent(file_get_contents($this->path));
}//end reloadContent()
/**
* Processes the file.
*
* @return void
*/
public function process()
{
if ($this->ignored === true) {
return;
}
if ($this->configCache['cache'] === false) {
parent::process();
return;
}
$hash = md5_file($this->path);
$hash .= fileperms($this->path);
$cache = Cache::get($this->path);
if ($cache !== false && $cache['hash'] === $hash) {
// We can't filter metrics, so just load all of them.
$this->metrics = $cache['metrics'];
if ($this->configCache['recordErrors'] === true) {
// Replay the cached errors and warnings to filter out the ones
// we don't need for this specific run.
$this->configCache['cache'] = false;
$this->replayErrors($cache['errors'], $cache['warnings']);
$this->configCache['cache'] = true;
} else {
$this->errorCount = $cache['errorCount'];
$this->warningCount = $cache['warningCount'];
$this->fixableCount = $cache['fixableCount'];
}
if (PHP_CODESNIFFER_VERBOSITY > 0
|| (PHP_CODESNIFFER_CBF === true && empty($this->config->files) === false)
) {
echo "[loaded from cache]... ";
}
$this->numTokens = $cache['numTokens'];
$this->fromCache = true;
return;
}//end if
if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo PHP_EOL;
}
parent::process();
$cache = [
'hash' => $hash,
'errors' => $this->errors,
'warnings' => $this->warnings,
'metrics' => $this->metrics,
'errorCount' => $this->errorCount,
'warningCount' => $this->warningCount,
'fixableCount' => $this->fixableCount,
'numTokens' => $this->numTokens,
];
Cache::set($this->path, $cache);
// During caching, we don't filter out errors in any way, so
// we need to do that manually now by replaying them.
if ($this->configCache['recordErrors'] === true) {
$this->configCache['cache'] = false;
$this->replayErrors($this->errors, $this->warnings);
$this->configCache['cache'] = true;
}
}//end process()
/**
* Clears and replays error and warnings for the file.
*
* Replaying errors and warnings allows for filtering rules to be changed
* and then errors and warnings to be reapplied with the new rules. This is
* particularly useful while caching.
*
* @param array $errors The list of errors to replay.
* @param array $warnings The list of warnings to replay.
*
* @return void
*/
private function replayErrors($errors, $warnings)
{
$this->errors = [];
$this->warnings = [];
$this->errorCount = 0;
$this->warningCount = 0;
$this->fixableCount = 0;
$this->replayingErrors = true;
foreach ($errors as $line => $lineErrors) {
foreach ($lineErrors as $column => $colErrors) {
foreach ($colErrors as $error) {
$this->activeListener = $error['listener'];
$this->addMessage(
true,
$error['message'],
$line,
$column,
$error['source'],
[],
$error['severity'],
$error['fixable']
);
}
}
}
foreach ($warnings as $line => $lineErrors) {
foreach ($lineErrors as $column => $colErrors) {
foreach ($colErrors as $error) {
$this->activeListener = $error['listener'];
$this->addMessage(
false,
$error['message'],
$line,
$column,
$error['source'],
[],
$error['severity'],
$error['fixable']
);
}
}
}
$this->replayingErrors = false;
}//end replayErrors()
}//end class
@@ -0,0 +1,156 @@
<?php
/**
* An abstract filter class for checking files and folders against exact matches.
*
* Supports both allowed files and disallowed files.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Filters;
use PHP_CodeSniffer\Util\Common;
abstract class ExactMatch extends Filter
{
/**
* A list of files to exclude.
*
* @var array
*/
private $disallowedFiles = null;
/**
* A list of files to include.
*
* If the allowed files list is empty, only files in the disallowed files list will be excluded.
*
* @var array
*/
private $allowedFiles = null;
/**
* Check whether the current element of the iterator is acceptable.
*
* If a file is both disallowed and allowed, it will be deemed unacceptable.
*
* @return bool
*/
public function accept()
{
if (parent::accept() === false) {
return false;
}
if ($this->disallowedFiles === null) {
$this->disallowedFiles = $this->getDisallowedFiles();
// BC-layer.
if ($this->disallowedFiles === null) {
$this->disallowedFiles = $this->getBlacklist();
}
}
if ($this->allowedFiles === null) {
$this->allowedFiles = $this->getAllowedFiles();
// BC-layer.
if ($this->allowedFiles === null) {
$this->allowedFiles = $this->getWhitelist();
}
}
$filePath = Common::realpath($this->current());
// If a file is both disallowed and allowed, the disallowed files list takes precedence.
if (isset($this->disallowedFiles[$filePath]) === true) {
return false;
}
if (empty($this->allowedFiles) === true && empty($this->disallowedFiles) === false) {
// We are only checking the disallowed files list, so everything else should be allowed.
return true;
}
return isset($this->allowedFiles[$filePath]);
}//end accept()
/**
* Returns an iterator for the current entry.
*
* Ensures that the disallowed files list and the allowed files list are preserved so they don't have
* to be generated each time.
*
* @return \RecursiveIterator
*/
public function getChildren()
{
$children = parent::getChildren();
$children->disallowedFiles = $this->disallowedFiles;
$children->allowedFiles = $this->allowedFiles;
return $children;
}//end getChildren()
/**
* Get a list of file paths to exclude.
*
* @deprecated 3.9.0 Implement the `getDisallowedFiles()` method instead.
* The `getDisallowedFiles()` method will be made abstract and therefore required
* in v4.0 and this method will be removed.
* If both methods are implemented, the new `getDisallowedFiles()` method will take precedence.
*
* @return array
*/
abstract protected function getBlacklist();
/**
* Get a list of file paths to include.
*
* @deprecated 3.9.0 Implement the `getAllowedFiles()` method instead.
* The `getAllowedFiles()` method will be made abstract and therefore required
* in v4.0 and this method will be removed.
* If both methods are implemented, the new `getAllowedFiles()` method will take precedence.
*
* @return array
*/
abstract protected function getWhitelist();
/**
* Get a list of file paths to exclude.
*
* @since 3.9.0 Replaces the deprecated `getBlacklist()` method.
*
* @return array|null
*/
protected function getDisallowedFiles()
{
return null;
}//end getDisallowedFiles()
/**
* Get a list of file paths to include.
*
* @since 3.9.0 Replaces the deprecated `getWhitelist()` method.
*
* @return array|null
*/
protected function getAllowedFiles()
{
return null;
}//end getAllowedFiles()
}//end class
@@ -0,0 +1,288 @@
<?php
/**
* A base filter class for filtering out files and folders during a run.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Filters;
use FilesystemIterator;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Util\Common;
use RecursiveDirectoryIterator;
use RecursiveFilterIterator;
use ReturnTypeWillChange;
class Filter extends RecursiveFilterIterator
{
/**
* The top-level path we are filtering.
*
* @var string
*/
protected $basedir = null;
/**
* The config data for the run.
*
* @var \PHP_CodeSniffer\Config
*/
protected $config = null;
/**
* The ruleset used for the run.
*
* @var \PHP_CodeSniffer\Ruleset
*/
protected $ruleset = null;
/**
* A list of ignore patterns that apply to directories only.
*
* @var array
*/
protected $ignoreDirPatterns = null;
/**
* A list of ignore patterns that apply to files only.
*
* @var array
*/
protected $ignoreFilePatterns = null;
/**
* A list of file paths we've already accepted.
*
* Used to ensure we aren't following circular symlinks.
*
* @var array
*/
protected $acceptedPaths = [];
/**
* Constructs a filter.
*
* @param \RecursiveIterator $iterator The iterator we are using to get file paths.
* @param string $basedir The top-level path we are filtering.
* @param \PHP_CodeSniffer\Config $config The config data for the run.
* @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
*
* @return void
*/
public function __construct($iterator, $basedir, Config $config, Ruleset $ruleset)
{
parent::__construct($iterator);
$this->basedir = $basedir;
$this->config = $config;
$this->ruleset = $ruleset;
}//end __construct()
/**
* Check whether the current element of the iterator is acceptable.
*
* Files are checked for allowed extensions and ignore patterns.
* Directories are checked for ignore patterns only.
*
* @return bool
*/
#[ReturnTypeWillChange]
public function accept()
{
$filePath = $this->current();
$realPath = Common::realpath($filePath);
if ($realPath !== false) {
// It's a real path somewhere, so record it
// to check for circular symlinks.
if (isset($this->acceptedPaths[$realPath]) === true) {
// We've been here before.
return false;
}
}
$filePath = $this->current();
if (is_dir($filePath) === true) {
if ($this->config->local === true) {
return false;
}
} else if ($this->shouldProcessFile($filePath) === false) {
return false;
}
if ($this->shouldIgnorePath($filePath) === true) {
return false;
}
$this->acceptedPaths[$realPath] = true;
return true;
}//end accept()
/**
* Returns an iterator for the current entry.
*
* Ensures that the ignore patterns are preserved so they don't have
* to be generated each time.
*
* @return \RecursiveIterator
*/
#[ReturnTypeWillChange]
public function getChildren()
{
$filterClass = get_called_class();
$children = new $filterClass(
new RecursiveDirectoryIterator($this->current(), (RecursiveDirectoryIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS)),
$this->basedir,
$this->config,
$this->ruleset
);
// Set the ignore patterns so we don't have to generate them again.
$children->ignoreDirPatterns = $this->ignoreDirPatterns;
$children->ignoreFilePatterns = $this->ignoreFilePatterns;
$children->acceptedPaths = $this->acceptedPaths;
return $children;
}//end getChildren()
/**
* Checks filtering rules to see if a file should be checked.
*
* Checks both file extension filters and path ignore filters.
*
* @param string $path The path to the file being checked.
*
* @return bool
*/
protected function shouldProcessFile($path)
{
// Check that the file's extension is one we are checking.
// We are strict about checking the extension and we don't
// let files through with no extension or that start with a dot.
$fileName = basename($path);
$fileParts = explode('.', $fileName);
if ($fileParts[0] === $fileName || $fileParts[0] === '') {
return false;
}
// Checking multi-part file extensions, so need to create a
// complete extension list and make sure one is allowed.
$extensions = [];
array_shift($fileParts);
while (empty($fileParts) === false) {
$extensions[implode('.', $fileParts)] = 1;
array_shift($fileParts);
}
$matches = array_intersect_key($extensions, $this->config->extensions);
if (empty($matches) === true) {
return false;
}
return true;
}//end shouldProcessFile()
/**
* Checks filtering rules to see if a path should be ignored.
*
* @param string $path The path to the file or directory being checked.
*
* @return bool
*/
protected function shouldIgnorePath($path)
{
if ($this->ignoreFilePatterns === null) {
$this->ignoreDirPatterns = [];
$this->ignoreFilePatterns = [];
$ignorePatterns = $this->config->ignored;
$rulesetIgnorePatterns = $this->ruleset->getIgnorePatterns();
foreach ($rulesetIgnorePatterns as $pattern => $type) {
// Ignore standard/sniff specific exclude rules.
if (is_array($type) === true) {
continue;
}
$ignorePatterns[$pattern] = $type;
}
foreach ($ignorePatterns as $pattern => $type) {
// If the ignore pattern ends with /* then it is ignoring an entire directory.
if (substr($pattern, -2) === '/*') {
// Need to check this pattern for dirs as well as individual file paths.
$this->ignoreFilePatterns[$pattern] = $type;
$pattern = substr($pattern, 0, -2).'(?=/|$)';
$this->ignoreDirPatterns[$pattern] = $type;
} else {
// This is a file-specific pattern, so only need to check this
// for individual file paths.
$this->ignoreFilePatterns[$pattern] = $type;
}
}
}//end if
$relativePath = $path;
if (strpos($path, $this->basedir) === 0) {
// The +1 cuts off the directory separator as well.
$relativePath = substr($path, (strlen($this->basedir) + 1));
}
if (is_dir($path) === true) {
$ignorePatterns = $this->ignoreDirPatterns;
} else {
$ignorePatterns = $this->ignoreFilePatterns;
}
foreach ($ignorePatterns as $pattern => $type) {
// Maintains backwards compatibility in case the ignore pattern does
// not have a relative/absolute value.
if (is_int($pattern) === true) {
$pattern = $type;
$type = 'absolute';
}
$replacements = [
'\\,' => ',',
'*' => '.*',
];
// We assume a / directory separator, as do the exclude rules
// most developers write, so we need a special case for any system
// that is different.
if (DIRECTORY_SEPARATOR === '\\') {
$replacements['/'] = '\\\\';
}
$pattern = strtr($pattern, $replacements);
if ($type === 'relative') {
$testPath = $relativePath;
} else {
$testPath = $path;
}
$pattern = '`'.$pattern.'`i';
if (preg_match($pattern, $testPath) === 1) {
return true;
}
}//end foreach
return false;
}//end shouldIgnorePath()
}//end class
@@ -0,0 +1,124 @@
<?php
/**
* A filter to only include files that have been modified or added in a Git repository.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Filters;
use PHP_CodeSniffer\Util\Common;
class GitModified extends ExactMatch
{
/**
* Get a list of file paths to exclude.
*
* @since 3.9.0
*
* @return array
*/
protected function getDisallowedFiles()
{
return [];
}//end getDisallowedFiles()
/**
* Get a list of file paths to exclude.
*
* @deprecated 3.9.0 Overload the `getDisallowedFiles()` method instead.
*
* @codeCoverageIgnore
*
* @return array
*/
protected function getBlacklist()
{
return $this->getDisallowedFiles();
}//end getBlacklist()
/**
* Get a list of file paths to include.
*
* @since 3.9.0
*
* @return array
*/
protected function getAllowedFiles()
{
$modified = [];
$cmd = 'git ls-files -o -m --exclude-standard -- '.escapeshellarg($this->basedir);
$output = $this->exec($cmd);
$basedir = $this->basedir;
if (is_dir($basedir) === false) {
$basedir = dirname($basedir);
}
foreach ($output as $path) {
$path = Common::realpath($path);
if ($path === false) {
continue;
}
do {
$modified[$path] = true;
$path = dirname($path);
} while ($path !== $basedir);
}
return $modified;
}//end getAllowedFiles()
/**
* Get a list of file paths to include.
*
* @deprecated 3.9.0 Overload the `getAllowedFiles()` method instead.
*
* @codeCoverageIgnore
*
* @return array
*/
protected function getWhitelist()
{
return $this->getAllowedFiles();
}//end getWhitelist()
/**
* Execute an external command.
*
* {@internal This method is only needed to allow for mocking the return value
* to test the class logic.}
*
* @param string $cmd Command.
*
* @return array
*/
protected function exec($cmd)
{
$output = [];
$lastLine = exec($cmd, $output);
if ($lastLine === false) {
return [];
}
return $output;
}//end exec()
}//end class
@@ -0,0 +1,126 @@
<?php
/**
* A filter to only include files that have been staged for commit in a Git repository.
*
* This filter is the ideal companion for your pre-commit git hook.
*
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2018 Juliette Reinders Folmer. All rights reserved.
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Filters;
use PHP_CodeSniffer\Util\Common;
class GitStaged extends ExactMatch
{
/**
* Get a list of file paths to exclude.
*
* @since 3.9.0
*
* @return array
*/
protected function getDisallowedFiles()
{
return [];
}//end getDisallowedFiles()
/**
* Get a list of file paths to exclude.
*
* @deprecated 3.9.0 Overload the `getDisallowedFiles()` method instead.
*
* @codeCoverageIgnore
*
* @return array
*/
protected function getBlacklist()
{
return $this->getDisallowedFiles();
}//end getBlacklist()
/**
* Get a list of file paths to include.
*
* @since 3.9.0
*
* @return array
*/
protected function getAllowedFiles()
{
$modified = [];
$cmd = 'git diff --cached --name-only -- '.escapeshellarg($this->basedir);
$output = $this->exec($cmd);
$basedir = $this->basedir;
if (is_dir($basedir) === false) {
$basedir = dirname($basedir);
}
foreach ($output as $path) {
$path = Common::realpath($path);
if ($path === false) {
// Skip deleted files.
continue;
}
do {
$modified[$path] = true;
$path = dirname($path);
} while ($path !== $basedir);
}
return $modified;
}//end getAllowedFiles()
/**
* Get a list of file paths to include.
*
* @deprecated 3.9.0 Overload the `getAllowedFiles()` method instead.
*
* @codeCoverageIgnore
*
* @return array
*/
protected function getWhitelist()
{
return $this->getAllowedFiles();
}//end getWhitelist()
/**
* Execute an external command.
*
* {@internal This method is only needed to allow for mocking the return value
* to test the class logic.}
*
* @param string $cmd Command.
*
* @return array
*/
protected function exec($cmd)
{
$output = [];
$lastLine = exec($cmd, $output);
if ($lastLine === false) {
return [];
}
return $output;
}//end exec()
}//end class
@@ -0,0 +1,846 @@
<?php
/**
* A helper class for fixing errors.
*
* Provides helper functions that act upon a token array and modify the file
* content.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Common;
class Fixer
{
/**
* Is the fixer enabled and fixing a file?
*
* Sniffs should check this value to ensure they are not
* doing extra processing to prepare for a fix when fixing is
* not required.
*
* @var boolean
*/
public $enabled = false;
/**
* The number of times we have looped over a file.
*
* @var integer
*/
public $loops = 0;
/**
* The file being fixed.
*
* @var \PHP_CodeSniffer\Files\File
*/
private $currentFile = null;
/**
* The list of tokens that make up the file contents.
*
* This is a simplified list which just contains the token content and nothing
* else. This is the array that is updated as fixes are made, not the file's
* token array. Imploding this array will give you the file content back.
*
* @var array<int, string>
*/
private $tokens = [];
/**
* A list of tokens that have already been fixed.
*
* We don't allow the same token to be fixed more than once each time
* through a file as this can easily cause conflicts between sniffs.
*
* @var int[]
*/
private $fixedTokens = [];
/**
* The last value of each fixed token.
*
* If a token is being "fixed" back to its last value, the fix is
* probably conflicting with another.
*
* @var array<int, array<string, mixed>>
*/
private $oldTokenValues = [];
/**
* A list of tokens that have been fixed during a changeset.
*
* All changes in changeset must be able to be applied, or else
* the entire changeset is rejected.
*
* @var array
*/
private $changeset = [];
/**
* Is there an open changeset.
*
* @var boolean
*/
private $inChangeset = false;
/**
* Is the current fixing loop in conflict?
*
* @var boolean
*/
private $inConflict = false;
/**
* The number of fixes that have been performed.
*
* @var integer
*/
private $numFixes = 0;
/**
* Starts fixing a new file.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being fixed.
*
* @return void
*/
public function startFile(File $phpcsFile)
{
$this->currentFile = $phpcsFile;
$this->numFixes = 0;
$this->fixedTokens = [];
$tokens = $phpcsFile->getTokens();
$this->tokens = [];
foreach ($tokens as $index => $token) {
if (isset($token['orig_content']) === true) {
$this->tokens[$index] = $token['orig_content'];
} else {
$this->tokens[$index] = $token['content'];
}
}
}//end startFile()
/**
* Attempt to fix the file by processing it until no fixes are made.
*
* @return boolean
*/
public function fixFile()
{
$fixable = $this->currentFile->getFixableCount();
if ($fixable === 0) {
// Nothing to fix.
return false;
}
$this->enabled = true;
$this->loops = 0;
while ($this->loops < 50) {
ob_start();
// Only needed once file content has changed.
$contents = $this->getContents();
if (PHP_CODESNIFFER_VERBOSITY > 2) {
@ob_end_clean();
echo '---START FILE CONTENT---'.PHP_EOL;
$lines = explode($this->currentFile->eolChar, $contents);
$max = strlen(count($lines));
foreach ($lines as $lineNum => $line) {
$lineNum++;
echo str_pad($lineNum, $max, ' ', STR_PAD_LEFT).'|'.$line.PHP_EOL;
}
echo '--- END FILE CONTENT ---'.PHP_EOL;
ob_start();
}
$this->inConflict = false;
$this->currentFile->ruleset->populateTokenListeners();
$this->currentFile->setContent($contents);
$this->currentFile->process();
ob_end_clean();
$this->loops++;
if (PHP_CODESNIFFER_CBF === true && PHP_CODESNIFFER_VERBOSITY > 0) {
echo "\r".str_repeat(' ', 80)."\r";
echo "\t=> Fixing file: $this->numFixes/$fixable violations remaining [made $this->loops pass";
if ($this->loops > 1) {
echo 'es';
}
echo ']... ';
if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo PHP_EOL;
}
}
if ($this->numFixes === 0 && $this->inConflict === false) {
// Nothing left to do.
break;
} else if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo "\t* fixed $this->numFixes violations, starting loop ".($this->loops + 1).' *'.PHP_EOL;
}
}//end while
$this->enabled = false;
if ($this->numFixes > 0) {
if (PHP_CODESNIFFER_VERBOSITY > 1) {
if (ob_get_level() > 0) {
ob_end_clean();
}
echo "\t*** Reached maximum number of loops with $this->numFixes violations left unfixed ***".PHP_EOL;
ob_start();
}
return false;
}
return true;
}//end fixFile()
/**
* Generates a text diff of the original file and the new content.
*
* @param string $filePath Optional file path to diff the file against.
* If not specified, the original version of the
* file will be used.
* @param boolean $colors Print coloured output or not.
*
* @return string
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException When the diff command fails.
*/
public function generateDiff($filePath=null, $colors=true)
{
if ($filePath === null) {
$filePath = $this->currentFile->getFilename();
}
$cwd = getcwd().DIRECTORY_SEPARATOR;
if (strpos($filePath, $cwd) === 0) {
$filename = substr($filePath, strlen($cwd));
} else {
$filename = $filePath;
}
$contents = $this->getContents();
$tempName = tempnam(sys_get_temp_dir(), 'phpcs-fixer');
$fixedFile = fopen($tempName, 'w');
fwrite($fixedFile, $contents);
// We must use something like shell_exec() or proc_open() because whitespace at the end
// of lines is critical to diff files.
// Using proc_open() instead of shell_exec improves performance on Windows significantly,
// while the results are the same (though more code is needed to get the results).
// This is specifically due to proc_open allowing to set the "bypass_shell" option.
$filename = escapeshellarg($filename);
$cmd = "diff -u -L$filename -LPHP_CodeSniffer $filename \"$tempName\"";
// Stream 0 = STDIN, 1 = STDOUT, 2 = STDERR.
$descriptorspec = [
0 => [
'pipe',
'r',
],
1 => [
'pipe',
'w',
],
2 => [
'pipe',
'w',
],
];
$options = null;
if (stripos(PHP_OS, 'WIN') === 0) {
$options = ['bypass_shell' => true];
}
$process = proc_open($cmd, $descriptorspec, $pipes, $cwd, null, $options);
if (is_resource($process) === false) {
throw new RuntimeException('Could not obtain a resource to execute the diff command.');
}
// We don't need these.
fclose($pipes[0]);
fclose($pipes[2]);
// Stdout will contain the actual diff.
$diff = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
fclose($fixedFile);
if (is_file($tempName) === true) {
unlink($tempName);
}
if ($diff === false || $diff === '') {
return '';
}
if ($colors === false) {
return $diff;
}
$diffLines = explode(PHP_EOL, $diff);
if (count($diffLines) === 1) {
// Seems to be required for cygwin.
$diffLines = explode("\n", $diff);
}
$diff = [];
foreach ($diffLines as $line) {
if (isset($line[0]) === true) {
switch ($line[0]) {
case '-':
$diff[] = "\033[31m$line\033[0m";
break;
case '+':
$diff[] = "\033[32m$line\033[0m";
break;
default:
$diff[] = $line;
}
}
}
$diff = implode(PHP_EOL, $diff);
return $diff;
}//end generateDiff()
/**
* Get a count of fixes that have been performed on the file.
*
* This value is reset every time a new file is started, or an existing
* file is restarted.
*
* @return int
*/
public function getFixCount()
{
return $this->numFixes;
}//end getFixCount()
/**
* Get the current content of the file, as a string.
*
* @return string
*/
public function getContents()
{
$contents = implode($this->tokens);
return $contents;
}//end getContents()
/**
* Get the current fixed content of a token.
*
* This function takes changesets into account so should be used
* instead of directly accessing the token array.
*
* @param int $stackPtr The position of the token in the token stack.
*
* @return string
*/
public function getTokenContent($stackPtr)
{
if ($this->inChangeset === true
&& isset($this->changeset[$stackPtr]) === true
) {
return $this->changeset[$stackPtr];
} else {
return $this->tokens[$stackPtr];
}
}//end getTokenContent()
/**
* Start recording actions for a changeset.
*
* @return void|false
*/
public function beginChangeset()
{
if ($this->inConflict === true) {
return false;
}
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if ($bt[1]['class'] === __CLASS__) {
$sniff = 'Fixer';
} else {
$sniff = Common::getSniffCode($bt[1]['class']);
}
$line = $bt[0]['line'];
@ob_end_clean();
echo "\t=> Changeset started by $sniff:$line".PHP_EOL;
ob_start();
}
$this->changeset = [];
$this->inChangeset = true;
}//end beginChangeset()
/**
* Stop recording actions for a changeset, and apply logged changes.
*
* @return boolean
*/
public function endChangeset()
{
if ($this->inConflict === true) {
return false;
}
$this->inChangeset = false;
$success = true;
$applied = [];
foreach ($this->changeset as $stackPtr => $content) {
$success = $this->replaceToken($stackPtr, $content);
if ($success === false) {
break;
} else {
$applied[] = $stackPtr;
}
}
if ($success === false) {
// Rolling back all changes.
foreach ($applied as $stackPtr) {
$this->revertToken($stackPtr);
}
if (PHP_CODESNIFFER_VERBOSITY > 1) {
@ob_end_clean();
echo "\t=> Changeset failed to apply".PHP_EOL;
ob_start();
}
} else if (PHP_CODESNIFFER_VERBOSITY > 1) {
$fixes = count($this->changeset);
@ob_end_clean();
echo "\t=> Changeset ended: $fixes changes applied".PHP_EOL;
ob_start();
}
$this->changeset = [];
return true;
}//end endChangeset()
/**
* Stop recording actions for a changeset, and discard logged changes.
*
* @return void
*/
public function rollbackChangeset()
{
$this->inChangeset = false;
$this->inConflict = false;
if (empty($this->changeset) === false) {
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$bt = debug_backtrace();
if ($bt[1]['class'] === 'PHP_CodeSniffer\Fixer') {
$sniff = $bt[2]['class'];
$line = $bt[1]['line'];
} else {
$sniff = $bt[1]['class'];
$line = $bt[0]['line'];
}
$sniff = Common::getSniffCode($sniff);
$numChanges = count($this->changeset);
@ob_end_clean();
echo "\t\tR: $sniff:$line rolled back the changeset ($numChanges changes)".PHP_EOL;
echo "\t=> Changeset rolled back".PHP_EOL;
ob_start();
}
$this->changeset = [];
}//end if
}//end rollbackChangeset()
/**
* Replace the entire contents of a token.
*
* @param int $stackPtr The position of the token in the token stack.
* @param string $content The new content of the token.
*
* @return bool If the change was accepted.
*/
public function replaceToken($stackPtr, $content)
{
if ($this->inConflict === true) {
return false;
}
if ($this->inChangeset === false
&& isset($this->fixedTokens[$stackPtr]) === true
) {
$indent = "\t";
if (empty($this->changeset) === false) {
$indent .= "\t";
}
if (PHP_CODESNIFFER_VERBOSITY > 1) {
@ob_end_clean();
echo "$indent* token $stackPtr has already been modified, skipping *".PHP_EOL;
ob_start();
}
return false;
}
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if ($bt[1]['class'] === 'PHP_CodeSniffer\Fixer') {
$sniff = $bt[2]['class'];
$line = $bt[1]['line'];
} else {
$sniff = $bt[1]['class'];
$line = $bt[0]['line'];
}
$sniff = Common::getSniffCode($sniff);
$tokens = $this->currentFile->getTokens();
$type = $tokens[$stackPtr]['type'];
$tokenLine = $tokens[$stackPtr]['line'];
$oldContent = Common::prepareForOutput($this->tokens[$stackPtr]);
$newContent = Common::prepareForOutput($content);
if (trim($this->tokens[$stackPtr]) === '' && isset($this->tokens[($stackPtr + 1)]) === true) {
// Add some context for whitespace only changes.
$append = Common::prepareForOutput($this->tokens[($stackPtr + 1)]);
$oldContent .= $append;
$newContent .= $append;
}
}//end if
if ($this->inChangeset === true) {
$this->changeset[$stackPtr] = $content;
if (PHP_CODESNIFFER_VERBOSITY > 1) {
@ob_end_clean();
echo "\t\tQ: $sniff:$line replaced token $stackPtr ($type on line $tokenLine) \"$oldContent\" => \"$newContent\"".PHP_EOL;
ob_start();
}
return true;
}
if (isset($this->oldTokenValues[$stackPtr]) === false) {
$this->oldTokenValues[$stackPtr] = [
'curr' => $content,
'prev' => $this->tokens[$stackPtr],
'loop' => $this->loops,
];
} else {
if ($this->oldTokenValues[$stackPtr]['prev'] === $content
&& $this->oldTokenValues[$stackPtr]['loop'] === ($this->loops - 1)
) {
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$indent = "\t";
if (empty($this->changeset) === false) {
$indent .= "\t";
}
$loop = $this->oldTokenValues[$stackPtr]['loop'];
@ob_end_clean();
echo "$indent**** $sniff:$line has possible conflict with another sniff on loop $loop; caused by the following change ****".PHP_EOL;
echo "$indent**** replaced token $stackPtr ($type on line $tokenLine) \"$oldContent\" => \"$newContent\" ****".PHP_EOL;
}
if ($this->oldTokenValues[$stackPtr]['loop'] >= ($this->loops - 1)) {
$this->inConflict = true;
if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo "$indent**** ignoring all changes until next loop ****".PHP_EOL;
}
}
if (PHP_CODESNIFFER_VERBOSITY > 1) {
ob_start();
}
return false;
}//end if
$this->oldTokenValues[$stackPtr]['prev'] = $this->oldTokenValues[$stackPtr]['curr'];
$this->oldTokenValues[$stackPtr]['curr'] = $content;
$this->oldTokenValues[$stackPtr]['loop'] = $this->loops;
}//end if
$this->fixedTokens[$stackPtr] = $this->tokens[$stackPtr];
$this->tokens[$stackPtr] = $content;
$this->numFixes++;
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$indent = "\t";
if (empty($this->changeset) === false) {
$indent .= "\tA: ";
}
if (ob_get_level() > 0) {
ob_end_clean();
}
echo "$indent$sniff:$line replaced token $stackPtr ($type on line $tokenLine) \"$oldContent\" => \"$newContent\"".PHP_EOL;
ob_start();
}
return true;
}//end replaceToken()
/**
* Reverts the previous fix made to a token.
*
* @param int $stackPtr The position of the token in the token stack.
*
* @return bool If a change was reverted.
*/
public function revertToken($stackPtr)
{
if (isset($this->fixedTokens[$stackPtr]) === false) {
return false;
}
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if ($bt[1]['class'] === 'PHP_CodeSniffer\Fixer') {
$sniff = $bt[2]['class'];
$line = $bt[1]['line'];
} else {
$sniff = $bt[1]['class'];
$line = $bt[0]['line'];
}
$sniff = Common::getSniffCode($sniff);
$tokens = $this->currentFile->getTokens();
$type = $tokens[$stackPtr]['type'];
$tokenLine = $tokens[$stackPtr]['line'];
$oldContent = Common::prepareForOutput($this->tokens[$stackPtr]);
$newContent = Common::prepareForOutput($this->fixedTokens[$stackPtr]);
if (trim($this->tokens[$stackPtr]) === '' && isset($tokens[($stackPtr + 1)]) === true) {
// Add some context for whitespace only changes.
$append = Common::prepareForOutput($this->tokens[($stackPtr + 1)]);
$oldContent .= $append;
$newContent .= $append;
}
}//end if
$this->tokens[$stackPtr] = $this->fixedTokens[$stackPtr];
unset($this->fixedTokens[$stackPtr]);
$this->numFixes--;
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$indent = "\t";
if (empty($this->changeset) === false) {
$indent .= "\tR: ";
}
@ob_end_clean();
echo "$indent$sniff:$line reverted token $stackPtr ($type on line $tokenLine) \"$oldContent\" => \"$newContent\"".PHP_EOL;
ob_start();
}
return true;
}//end revertToken()
/**
* Replace the content of a token with a part of its current content.
*
* @param int $stackPtr The position of the token in the token stack.
* @param int $start The first character to keep.
* @param int $length The number of characters to keep. If NULL, the content of
* the token from $start to the end of the content is kept.
*
* @return bool If the change was accepted.
*/
public function substrToken($stackPtr, $start, $length=null)
{
$current = $this->getTokenContent($stackPtr);
if ($length === null) {
$newContent = substr($current, $start);
} else {
$newContent = substr($current, $start, $length);
}
return $this->replaceToken($stackPtr, $newContent);
}//end substrToken()
/**
* Adds a newline to end of a token's content.
*
* @param int $stackPtr The position of the token in the token stack.
*
* @return bool If the change was accepted.
*/
public function addNewline($stackPtr)
{
$current = $this->getTokenContent($stackPtr);
return $this->replaceToken($stackPtr, $current.$this->currentFile->eolChar);
}//end addNewline()
/**
* Adds a newline to the start of a token's content.
*
* @param int $stackPtr The position of the token in the token stack.
*
* @return bool If the change was accepted.
*/
public function addNewlineBefore($stackPtr)
{
$current = $this->getTokenContent($stackPtr);
return $this->replaceToken($stackPtr, $this->currentFile->eolChar.$current);
}//end addNewlineBefore()
/**
* Adds content to the end of a token's current content.
*
* @param int $stackPtr The position of the token in the token stack.
* @param string $content The content to add.
*
* @return bool If the change was accepted.
*/
public function addContent($stackPtr, $content)
{
$current = $this->getTokenContent($stackPtr);
return $this->replaceToken($stackPtr, $current.$content);
}//end addContent()
/**
* Adds content to the start of a token's current content.
*
* @param int $stackPtr The position of the token in the token stack.
* @param string $content The content to add.
*
* @return bool If the change was accepted.
*/
public function addContentBefore($stackPtr, $content)
{
$current = $this->getTokenContent($stackPtr);
return $this->replaceToken($stackPtr, $content.$current);
}//end addContentBefore()
/**
* Adjust the indent of a code block.
*
* @param int $start The position of the token in the token stack
* to start adjusting the indent from.
* @param int $end The position of the token in the token stack
* to end adjusting the indent.
* @param int $change The number of spaces to adjust the indent by
* (positive or negative).
*
* @return void
*/
public function changeCodeBlockIndent($start, $end, $change)
{
$tokens = $this->currentFile->getTokens();
$baseIndent = '';
if ($change > 0) {
$baseIndent = str_repeat(' ', $change);
}
$useChangeset = false;
if ($this->inChangeset === false) {
$this->beginChangeset();
$useChangeset = true;
}
for ($i = $start; $i <= $end; $i++) {
if ($tokens[$i]['column'] !== 1
|| $tokens[($i + 1)]['line'] !== $tokens[$i]['line']
) {
continue;
}
$length = 0;
if ($tokens[$i]['code'] === T_WHITESPACE
|| $tokens[$i]['code'] === T_DOC_COMMENT_WHITESPACE
) {
$length = $tokens[$i]['length'];
$padding = ($length + $change);
if ($padding > 0) {
$padding = str_repeat(' ', $padding);
} else {
$padding = '';
}
$newContent = $padding.ltrim($tokens[$i]['content']);
} else {
$newContent = $baseIndent.$tokens[$i]['content'];
}
$this->replaceToken($i, $newContent);
}//end for
if ($useChangeset === true) {
$this->endChangeset();
}
}//end changeCodeBlockIndent()
}//end class
@@ -0,0 +1,128 @@
<?php
/**
* The base class for all PHP_CodeSniffer documentation generators.
*
* Documentation generators are used to print documentation about code sniffs
* in a standard.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @copyright 2024 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Generators;
use DOMDocument;
use DOMNode;
use PHP_CodeSniffer\Autoload;
use PHP_CodeSniffer\Ruleset;
abstract class Generator
{
/**
* The ruleset used for the run.
*
* @var \PHP_CodeSniffer\Ruleset
*/
public $ruleset = null;
/**
* XML documentation files used to produce the final output.
*
* @var string[]
*/
public $docFiles = [];
/**
* Constructs a doc generator.
*
* @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
*
* @see generate()
*/
public function __construct(Ruleset $ruleset)
{
$this->ruleset = $ruleset;
$find = [
DIRECTORY_SEPARATOR.'Sniffs'.DIRECTORY_SEPARATOR,
'Sniff.php',
];
$replace = [
DIRECTORY_SEPARATOR.'Docs'.DIRECTORY_SEPARATOR,
'Standard.xml',
];
foreach ($ruleset->sniffs as $className => $sniffClass) {
$file = Autoload::getLoadedFileName($className);
$docFile = str_replace($find, $replace, $file);
if (is_file($docFile) === true) {
$this->docFiles[] = $docFile;
}
}
// Always present the docs in a consistent alphabetical order.
sort($this->docFiles, (SORT_NATURAL | SORT_FLAG_CASE));
}//end __construct()
/**
* Retrieves the title of the sniff from the DOMNode supplied.
*
* @param \DOMNode $doc The DOMNode object for the sniff.
* It represents the "documentation" tag in the XML
* standard file.
*
* @return string
*/
protected function getTitle(DOMNode $doc)
{
return $doc->getAttribute('title');
}//end getTitle()
/**
* Generates the documentation for a standard.
*
* It's probably wise for doc generators to override this method so they
* have control over how the docs are produced. Otherwise, the processSniff
* method should be overridden to output content for each sniff.
*
* @return void
* @see processSniff()
*/
public function generate()
{
foreach ($this->docFiles as $file) {
$doc = new DOMDocument();
$doc->load($file);
$documentation = $doc->getElementsByTagName('documentation')->item(0);
$this->processSniff($documentation);
}
}//end generate()
/**
* Process the documentation for a single sniff.
*
* Doc generators must implement this function to produce output.
*
* @param \DOMNode $doc The DOMNode object for the sniff.
* It represents the "documentation" tag in the XML
* standard file.
*
* @return void
* @see generate()
*/
abstract protected function processSniff(DOMNode $doc);
}//end class
@@ -0,0 +1,316 @@
<?php
/**
* A doc generator that outputs documentation in one big HTML file.
*
* Output is in one large HTML file and is designed for you to style with
* your own stylesheet. It contains a table of contents at the top with anchors
* to each sniff.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @copyright 2024 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Generators;
use DOMDocument;
use DOMNode;
use PHP_CodeSniffer\Config;
class HTML extends Generator
{
/**
* Stylesheet for the HTML output.
*
* @var string
*/
const STYLESHEET = '<style>
body {
background-color: #FFFFFF;
font-size: 14px;
font-family: Arial, Helvetica, sans-serif;
color: #000000;
}
h1 {
color: #666666;
font-size: 20px;
font-weight: bold;
margin-top: 0px;
background-color: #E6E7E8;
padding: 20px;
border: 1px solid #BBBBBB;
}
h2 {
color: #00A5E3;
font-size: 16px;
font-weight: normal;
margin-top: 50px;
}
.code-comparison {
width: 100%;
}
.code-comparison td {
border: 1px solid #CCCCCC;
}
.code-comparison-title, .code-comparison-code {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
color: #000000;
vertical-align: top;
padding: 4px;
width: 50%;
background-color: #F1F1F1;
line-height: 15px;
}
.code-comparison-code {
font-family: Courier;
background-color: #F9F9F9;
}
.code-comparison-highlight {
background-color: #DDF1F7;
border: 1px solid #00A5E3;
line-height: 15px;
}
.tag-line {
text-align: center;
width: 100%;
margin-top: 30px;
font-size: 12px;
}
.tag-line a {
color: #000000;
}
</style>';
/**
* Generates the documentation for a standard.
*
* @return void
* @see processSniff()
*/
public function generate()
{
if (empty($this->docFiles) === true) {
return;
}
ob_start();
$this->printHeader();
$this->printToc();
foreach ($this->docFiles as $file) {
$doc = new DOMDocument();
$doc->load($file);
$documentation = $doc->getElementsByTagName('documentation')->item(0);
$this->processSniff($documentation);
}
$this->printFooter();
$content = ob_get_contents();
ob_end_clean();
echo $content;
}//end generate()
/**
* Print the header of the HTML page.
*
* @return void
*/
protected function printHeader()
{
$standard = $this->ruleset->name;
echo '<html>'.PHP_EOL;
echo ' <head>'.PHP_EOL;
echo " <title>$standard Coding Standards</title>".PHP_EOL;
echo ' '.str_replace("\n", PHP_EOL, self::STYLESHEET).PHP_EOL;
echo ' </head>'.PHP_EOL;
echo ' <body>'.PHP_EOL;
echo " <h1>$standard Coding Standards</h1>".PHP_EOL;
}//end printHeader()
/**
* Print the table of contents for the standard.
*
* The TOC is just an unordered list of bookmarks to sniffs on the page.
*
* @return void
*/
protected function printToc()
{
// Only show a TOC when there are two or more docs to display.
if (count($this->docFiles) < 2) {
return;
}
echo ' <h2>Table of Contents</h2>'.PHP_EOL;
echo ' <ul class="toc">'.PHP_EOL;
foreach ($this->docFiles as $file) {
$doc = new DOMDocument();
$doc->load($file);
$documentation = $doc->getElementsByTagName('documentation')->item(0);
$title = $this->getTitle($documentation);
echo ' <li><a href="#'.str_replace(' ', '-', $title)."\">$title</a></li>".PHP_EOL;
}
echo ' </ul>'.PHP_EOL;
}//end printToc()
/**
* Print the footer of the HTML page.
*
* @return void
*/
protected function printFooter()
{
// Turn off errors so we don't get timezone warnings if people
// don't have their timezone set.
$errorLevel = error_reporting(0);
echo ' <div class="tag-line">';
echo 'Documentation generated on '.date('r');
echo ' by <a href="https://github.com/PHPCSStandards/PHP_CodeSniffer">PHP_CodeSniffer '.Config::VERSION.'</a>';
echo '</div>'.PHP_EOL;
error_reporting($errorLevel);
echo ' </body>'.PHP_EOL;
echo '</html>'.PHP_EOL;
}//end printFooter()
/**
* Process the documentation for a single sniff.
*
* @param \DOMNode $doc The DOMNode object for the sniff.
* It represents the "documentation" tag in the XML
* standard file.
*
* @return void
*/
public function processSniff(DOMNode $doc)
{
$title = $this->getTitle($doc);
echo ' <a name="'.str_replace(' ', '-', $title).'" />'.PHP_EOL;
echo " <h2>$title</h2>".PHP_EOL;
foreach ($doc->childNodes as $node) {
if ($node->nodeName === 'standard') {
$this->printTextBlock($node);
} else if ($node->nodeName === 'code_comparison') {
$this->printCodeComparisonBlock($node);
}
}
}//end processSniff()
/**
* Print a text block found in a standard.
*
* @param \DOMNode $node The DOMNode object for the text block.
*
* @return void
*/
protected function printTextBlock(DOMNode $node)
{
$content = trim($node->nodeValue);
$content = htmlspecialchars($content, (ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401));
// Allow only em tags.
$content = str_replace('&lt;em&gt;', '<em>', $content);
$content = str_replace('&lt;/em&gt;', '</em>', $content);
$nodeLines = explode("\n", $content);
$lineCount = count($nodeLines);
$lines = [];
for ($i = 0; $i < $lineCount; $i++) {
$currentLine = trim($nodeLines[$i]);
if (isset($nodeLines[($i + 1)]) === false) {
// We're at the end of the text, just add the line.
$lines[] = $currentLine;
} else {
$nextLine = trim($nodeLines[($i + 1)]);
if ($nextLine === '') {
// Next line is a blank line, end the paragraph and start a new one.
// Also skip over the blank line.
$lines[] = $currentLine.'</p>'.PHP_EOL.' <p class="text">';
++$i;
} else {
// Next line is not blank, so just add a line break.
$lines[] = $currentLine.'<br/>'.PHP_EOL;
}
}
}
echo ' <p class="text">'.implode('', $lines).'</p>'.PHP_EOL;
}//end printTextBlock()
/**
* Print a code comparison block found in a standard.
*
* @param \DOMNode $node The DOMNode object for the code comparison block.
*
* @return void
*/
protected function printCodeComparisonBlock(DOMNode $node)
{
$codeBlocks = $node->getElementsByTagName('code');
$firstTitle = trim($codeBlocks->item(0)->getAttribute('title'));
$firstTitle = str_replace(' ', '&nbsp;&nbsp;', $firstTitle);
$first = trim($codeBlocks->item(0)->nodeValue);
$first = str_replace('<?php', '&lt;?php', $first);
$first = str_replace("\n", '</br>', $first);
$first = str_replace(' ', '&nbsp;', $first);
$first = str_replace('<em>', '<span class="code-comparison-highlight">', $first);
$first = str_replace('</em>', '</span>', $first);
$secondTitle = trim($codeBlocks->item(1)->getAttribute('title'));
$secondTitle = str_replace(' ', '&nbsp;&nbsp;', $secondTitle);
$second = trim($codeBlocks->item(1)->nodeValue);
$second = str_replace('<?php', '&lt;?php', $second);
$second = str_replace("\n", '</br>', $second);
$second = str_replace(' ', '&nbsp;', $second);
$second = str_replace('<em>', '<span class="code-comparison-highlight">', $second);
$second = str_replace('</em>', '</span>', $second);
echo ' <table class="code-comparison">'.PHP_EOL;
echo ' <tr>'.PHP_EOL;
echo " <td class=\"code-comparison-title\">$firstTitle</td>".PHP_EOL;
echo " <td class=\"code-comparison-title\">$secondTitle</td>".PHP_EOL;
echo ' </tr>'.PHP_EOL;
echo ' <tr>'.PHP_EOL;
echo " <td class=\"code-comparison-code\">$first</td>".PHP_EOL;
echo " <td class=\"code-comparison-code\">$second</td>".PHP_EOL;
echo ' </tr>'.PHP_EOL;
echo ' </table>'.PHP_EOL;
}//end printCodeComparisonBlock()
}//end class
@@ -0,0 +1,195 @@
<?php
/**
* A doc generator that outputs documentation in Markdown format.
*
* @author Stefano Kowalke <blueduck@gmx.net>
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2014 Arroba IT
* @copyright 2024 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Generators;
use DOMDocument;
use DOMNode;
use PHP_CodeSniffer\Config;
class Markdown extends Generator
{
/**
* Generates the documentation for a standard.
*
* @return void
* @see processSniff()
*/
public function generate()
{
if (empty($this->docFiles) === true) {
return;
}
ob_start();
$this->printHeader();
foreach ($this->docFiles as $file) {
$doc = new DOMDocument();
$doc->load($file);
$documentation = $doc->getElementsByTagName('documentation')->item(0);
$this->processSniff($documentation);
}
$this->printFooter();
$content = ob_get_contents();
ob_end_clean();
echo $content;
}//end generate()
/**
* Print the markdown header.
*
* @return void
*/
protected function printHeader()
{
$standard = $this->ruleset->name;
echo "# $standard Coding Standard".PHP_EOL;
}//end printHeader()
/**
* Print the markdown footer.
*
* @return void
*/
protected function printFooter()
{
// Turn off errors so we don't get timezone warnings if people
// don't have their timezone set.
$errorLevel = error_reporting(0);
echo PHP_EOL.'Documentation generated on '.date('r');
echo ' by [PHP_CodeSniffer '.Config::VERSION.'](https://github.com/PHPCSStandards/PHP_CodeSniffer)'.PHP_EOL;
error_reporting($errorLevel);
}//end printFooter()
/**
* Process the documentation for a single sniff.
*
* @param \DOMNode $doc The DOMNode object for the sniff.
* It represents the "documentation" tag in the XML
* standard file.
*
* @return void
*/
protected function processSniff(DOMNode $doc)
{
$title = $this->getTitle($doc);
echo PHP_EOL."## $title".PHP_EOL.PHP_EOL;
foreach ($doc->childNodes as $node) {
if ($node->nodeName === 'standard') {
$this->printTextBlock($node);
} else if ($node->nodeName === 'code_comparison') {
$this->printCodeComparisonBlock($node);
}
}
}//end processSniff()
/**
* Print a text block found in a standard.
*
* @param \DOMNode $node The DOMNode object for the text block.
*
* @return void
*/
protected function printTextBlock(DOMNode $node)
{
$content = trim($node->nodeValue);
$content = htmlspecialchars($content, (ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401));
$content = str_replace('&lt;em&gt;', '*', $content);
$content = str_replace('&lt;/em&gt;', '*', $content);
$nodeLines = explode("\n", $content);
$lineCount = count($nodeLines);
$lines = [];
for ($i = 0; $i < $lineCount; $i++) {
$currentLine = trim($nodeLines[$i]);
if ($currentLine === '') {
// The text contained a blank line. Respect this.
$lines[] = '';
continue;
}
// Check if the _next_ line is blank.
if (isset($nodeLines[($i + 1)]) === false
|| trim($nodeLines[($i + 1)]) === ''
) {
// Next line is blank, just add the line.
$lines[] = $currentLine;
} else {
// Ensure that line breaks are respected in markdown.
$lines[] = $currentLine.' ';
}
}
echo implode(PHP_EOL, $lines).PHP_EOL;
}//end printTextBlock()
/**
* Print a code comparison block found in a standard.
*
* @param \DOMNode $node The DOMNode object for the code comparison block.
*
* @return void
*/
protected function printCodeComparisonBlock(DOMNode $node)
{
$codeBlocks = $node->getElementsByTagName('code');
$firstTitle = trim($codeBlocks->item(0)->getAttribute('title'));
$firstTitle = str_replace(' ', '&nbsp;&nbsp;', $firstTitle);
$first = trim($codeBlocks->item(0)->nodeValue);
$first = str_replace("\n", PHP_EOL.' ', $first);
$first = str_replace('<em>', '', $first);
$first = str_replace('</em>', '', $first);
$secondTitle = trim($codeBlocks->item(1)->getAttribute('title'));
$secondTitle = str_replace(' ', '&nbsp;&nbsp;', $secondTitle);
$second = trim($codeBlocks->item(1)->nodeValue);
$second = str_replace("\n", PHP_EOL.' ', $second);
$second = str_replace('<em>', '', $second);
$second = str_replace('</em>', '', $second);
echo ' <table>'.PHP_EOL;
echo ' <tr>'.PHP_EOL;
echo " <th>$firstTitle</th>".PHP_EOL;
echo " <th>$secondTitle</th>".PHP_EOL;
echo ' </tr>'.PHP_EOL;
echo ' <tr>'.PHP_EOL;
echo '<td>'.PHP_EOL.PHP_EOL;
echo " $first".PHP_EOL.PHP_EOL;
echo '</td>'.PHP_EOL;
echo '<td>'.PHP_EOL.PHP_EOL;
echo " $second".PHP_EOL.PHP_EOL;
echo '</td>'.PHP_EOL;
echo ' </tr>'.PHP_EOL;
echo ' </table>'.PHP_EOL;
}//end printCodeComparisonBlock()
}//end class
@@ -0,0 +1,259 @@
<?php
/**
* A doc generator that outputs text-based documentation.
*
* Output is designed to be displayed in a terminal and is wrapped to 100 characters.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @copyright 2024 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Generators;
use DOMNode;
class Text extends Generator
{
/**
* Process the documentation for a single sniff.
*
* @param \DOMNode $doc The DOMNode object for the sniff.
* It represents the "documentation" tag in the XML
* standard file.
*
* @return void
*/
public function processSniff(DOMNode $doc)
{
$this->printTitle($doc);
foreach ($doc->childNodes as $node) {
if ($node->nodeName === 'standard') {
$this->printTextBlock($node);
} else if ($node->nodeName === 'code_comparison') {
$this->printCodeComparisonBlock($node);
}
}
}//end processSniff()
/**
* Prints the title area for a single sniff.
*
* @param \DOMNode $doc The DOMNode object for the sniff.
* It represents the "documentation" tag in the XML
* standard file.
*
* @return void
*/
protected function printTitle(DOMNode $doc)
{
$title = $this->getTitle($doc);
$standard = $this->ruleset->name;
$displayTitle = "$standard CODING STANDARD: $title";
$titleLength = strlen($displayTitle);
echo PHP_EOL;
echo str_repeat('-', ($titleLength + 4));
echo strtoupper(PHP_EOL."| $displayTitle |".PHP_EOL);
echo str_repeat('-', ($titleLength + 4));
echo PHP_EOL.PHP_EOL;
}//end printTitle()
/**
* Print a text block found in a standard.
*
* @param \DOMNode $node The DOMNode object for the text block.
*
* @return void
*/
protected function printTextBlock(DOMNode $node)
{
$text = trim($node->nodeValue);
$text = str_replace('<em>', '*', $text);
$text = str_replace('</em>', '*', $text);
$nodeLines = explode("\n", $text);
$lines = [];
foreach ($nodeLines as $currentLine) {
$currentLine = trim($currentLine);
if ($currentLine === '') {
// The text contained a blank line. Respect this.
$lines[] = '';
continue;
}
$tempLine = '';
$words = explode(' ', $currentLine);
foreach ($words as $word) {
$currentLength = strlen($tempLine.$word);
if ($currentLength < 99) {
$tempLine .= $word.' ';
continue;
}
if ($currentLength === 99 || $currentLength === 100) {
// We are already at the edge, so we are done.
$lines[] = $tempLine.$word;
$tempLine = '';
} else {
$lines[] = rtrim($tempLine);
$tempLine = $word.' ';
}
}//end foreach
if ($tempLine !== '') {
$lines[] = rtrim($tempLine);
}
}//end foreach
echo implode(PHP_EOL, $lines).PHP_EOL.PHP_EOL;
}//end printTextBlock()
/**
* Print a code comparison block found in a standard.
*
* @param \DOMNode $node The DOMNode object for the code comparison block.
*
* @return void
*/
protected function printCodeComparisonBlock(DOMNode $node)
{
$codeBlocks = $node->getElementsByTagName('code');
$first = trim($codeBlocks->item(0)->nodeValue);
$firstTitle = trim($codeBlocks->item(0)->getAttribute('title'));
$firstTitleLines = [];
$tempTitle = '';
$words = explode(' ', $firstTitle);
foreach ($words as $word) {
if (strlen($tempTitle.$word) >= 45) {
if (strlen($tempTitle.$word) === 45) {
// Adding the extra space will push us to the edge
// so we are done.
$firstTitleLines[] = $tempTitle.$word;
$tempTitle = '';
} else if (strlen($tempTitle.$word) === 46) {
// We are already at the edge, so we are done.
$firstTitleLines[] = $tempTitle.$word;
$tempTitle = '';
} else {
$firstTitleLines[] = $tempTitle;
$tempTitle = $word.' ';
}
} else {
$tempTitle .= $word.' ';
}
}//end foreach
if ($tempTitle !== '') {
$firstTitleLines[] = $tempTitle;
}
$first = str_replace('<em>', '', $first);
$first = str_replace('</em>', '', $first);
$firstLines = explode("\n", $first);
$second = trim($codeBlocks->item(1)->nodeValue);
$secondTitle = trim($codeBlocks->item(1)->getAttribute('title'));
$secondTitleLines = [];
$tempTitle = '';
$words = explode(' ', $secondTitle);
foreach ($words as $word) {
if (strlen($tempTitle.$word) >= 45) {
if (strlen($tempTitle.$word) === 45) {
// Adding the extra space will push us to the edge
// so we are done.
$secondTitleLines[] = $tempTitle.$word;
$tempTitle = '';
} else if (strlen($tempTitle.$word) === 46) {
// We are already at the edge, so we are done.
$secondTitleLines[] = $tempTitle.$word;
$tempTitle = '';
} else {
$secondTitleLines[] = $tempTitle;
$tempTitle = $word.' ';
}
} else {
$tempTitle .= $word.' ';
}
}//end foreach
if ($tempTitle !== '') {
$secondTitleLines[] = $tempTitle;
}
$second = str_replace('<em>', '', $second);
$second = str_replace('</em>', '', $second);
$secondLines = explode("\n", $second);
$maxCodeLines = max(count($firstLines), count($secondLines));
$maxTitleLines = max(count($firstTitleLines), count($secondTitleLines));
echo str_repeat('-', 41);
echo ' CODE COMPARISON ';
echo str_repeat('-', 42).PHP_EOL;
for ($i = 0; $i < $maxTitleLines; $i++) {
if (isset($firstTitleLines[$i]) === true) {
$firstLineText = $firstTitleLines[$i];
} else {
$firstLineText = '';
}
if (isset($secondTitleLines[$i]) === true) {
$secondLineText = $secondTitleLines[$i];
} else {
$secondLineText = '';
}
echo '| ';
echo $firstLineText.str_repeat(' ', (46 - strlen($firstLineText)));
echo ' | ';
echo $secondLineText.str_repeat(' ', (47 - strlen($secondLineText)));
echo ' |'.PHP_EOL;
}//end for
echo str_repeat('-', 100).PHP_EOL;
for ($i = 0; $i < $maxCodeLines; $i++) {
if (isset($firstLines[$i]) === true) {
$firstLineText = $firstLines[$i];
} else {
$firstLineText = '';
}
if (isset($secondLines[$i]) === true) {
$secondLineText = $secondLines[$i];
} else {
$secondLineText = '';
}
echo '| ';
echo $firstLineText.str_repeat(' ', max(0, (47 - strlen($firstLineText))));
echo '| ';
echo $secondLineText.str_repeat(' ', max(0, (48 - strlen($secondLineText))));
echo '|'.PHP_EOL;
}//end for
echo str_repeat('-', 100).PHP_EOL.PHP_EOL;
}//end printCodeComparisonBlock()
}//end class
@@ -0,0 +1,445 @@
<?php
/**
* Manages reporting of errors and warnings.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer;
use PHP_CodeSniffer\Exceptions\DeepExitException;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Reports\Report;
use PHP_CodeSniffer\Util\Common;
class Reporter
{
/**
* The config data for the run.
*
* @var \PHP_CodeSniffer\Config
*/
public $config = null;
/**
* Total number of files that contain errors or warnings.
*
* @var integer
*/
public $totalFiles = 0;
/**
* Total number of errors found during the run.
*
* @var integer
*/
public $totalErrors = 0;
/**
* Total number of warnings found during the run.
*
* @var integer
*/
public $totalWarnings = 0;
/**
* Total number of errors/warnings that can be fixed.
*
* @var integer
*/
public $totalFixable = 0;
/**
* Total number of errors/warnings that were fixed.
*
* @var integer
*/
public $totalFixed = 0;
/**
* When the PHPCS run started.
*
* @var float
*/
public static $startTime = 0;
/**
* A cache of report objects.
*
* @var array
*/
private $reports = [];
/**
* A cache of opened temporary files.
*
* @var array
*/
private $tmpFiles = [];
/**
* Initialise the reporter.
*
* All reports specified in the config will be created and their
* output file (or a temp file if none is specified) initialised by
* clearing the current contents.
*
* @param \PHP_CodeSniffer\Config $config The config data for the run.
*
* @return void
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException If a custom report class could not be found.
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If a report class is incorrectly set up.
*/
public function __construct(Config $config)
{
$this->config = $config;
foreach ($config->reports as $type => $output) {
if ($output === null) {
$output = $config->reportFile;
}
$reportClassName = '';
if (strpos($type, '.') !== false) {
// This is a path to a custom report class.
$filename = realpath($type);
if ($filename === false) {
$error = "ERROR: Custom report \"$type\" not found".PHP_EOL;
throw new DeepExitException($error, 3);
}
$reportClassName = Autoload::loadFile($filename);
} else if (class_exists('PHP_CodeSniffer\Reports\\'.ucfirst($type)) === true) {
// PHPCS native report.
$reportClassName = 'PHP_CodeSniffer\Reports\\'.ucfirst($type);
} else if (class_exists($type) === true) {
// FQN of a custom report.
$reportClassName = $type;
} else {
// OK, so not a FQN, try and find the report using the registered namespaces.
$registeredNamespaces = Autoload::getSearchPaths();
$trimmedType = ltrim($type, '\\');
foreach ($registeredNamespaces as $nsPrefix) {
if ($nsPrefix === '') {
continue;
}
if (class_exists($nsPrefix.'\\'.$trimmedType) === true) {
$reportClassName = $nsPrefix.'\\'.$trimmedType;
break;
}
}
}//end if
if ($reportClassName === '') {
$error = "ERROR: Class file for report \"$type\" not found".PHP_EOL;
throw new DeepExitException($error, 3);
}
$reportClass = new $reportClassName();
if (($reportClass instanceof Report) === false) {
throw new RuntimeException('Class "'.$reportClassName.'" must implement the "PHP_CodeSniffer\Report" interface.');
}
$this->reports[$type] = [
'output' => $output,
'class' => $reportClass,
];
if ($output === null) {
// Using a temp file.
// This needs to be set in the constructor so that all
// child procs use the same report file when running in parallel.
$this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs');
file_put_contents($this->tmpFiles[$type], '');
} else {
file_put_contents($output, '');
}
}//end foreach
}//end __construct()
/**
* Generates and prints final versions of all reports.
*
* Returns TRUE if any of the reports output content to the screen
* or FALSE if all reports were silently printed to a file.
*
* @return bool
*/
public function printReports()
{
$toScreen = false;
foreach ($this->reports as $type => $report) {
if ($report['output'] === null) {
$toScreen = true;
}
$this->printReport($type);
}
return $toScreen;
}//end printReports()
/**
* Generates and prints a single final report.
*
* @param string $report The report type to print.
*
* @return void
*/
public function printReport($report)
{
$reportClass = $this->reports[$report]['class'];
$reportFile = $this->reports[$report]['output'];
if ($reportFile !== null) {
$filename = $reportFile;
$toScreen = false;
} else {
if (isset($this->tmpFiles[$report]) === true) {
$filename = $this->tmpFiles[$report];
} else {
$filename = null;
}
$toScreen = true;
}
$reportCache = '';
if ($filename !== null) {
$reportCache = file_get_contents($filename);
}
ob_start();
$reportClass->generate(
$reportCache,
$this->totalFiles,
$this->totalErrors,
$this->totalWarnings,
$this->totalFixable,
$this->config->showSources,
$this->config->reportWidth,
$this->config->interactive,
$toScreen
);
$generatedReport = ob_get_contents();
ob_end_clean();
if ($this->config->colors !== true || $reportFile !== null) {
$generatedReport = Common::stripColors($generatedReport);
}
if ($reportFile !== null) {
if (PHP_CODESNIFFER_VERBOSITY > 0) {
echo $generatedReport;
}
file_put_contents($reportFile, $generatedReport.PHP_EOL);
} else {
echo $generatedReport;
if ($filename !== null && file_exists($filename) === true) {
unlink($filename);
unset($this->tmpFiles[$report]);
}
}
}//end printReport()
/**
* Caches the result of a single processed file for all reports.
*
* The report content that is generated is appended to the output file
* assigned to each report. This content may be an intermediate report format
* and not reflect the final report output.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed.
*
* @return void
*/
public function cacheFileReport(File $phpcsFile)
{
if (isset($this->config->reports) === false) {
// This happens during unit testing, or any time someone just wants
// the error data and not the printed report.
return;
}
$reportData = $this->prepareFileReport($phpcsFile);
$errorsShown = false;
foreach ($this->reports as $type => $report) {
$reportClass = $report['class'];
ob_start();
$result = $reportClass->generateFileReport($reportData, $phpcsFile, $this->config->showSources, $this->config->reportWidth);
if ($result === true) {
$errorsShown = true;
}
$generatedReport = ob_get_contents();
ob_end_clean();
if ($report['output'] === null) {
// Using a temp file.
if (isset($this->tmpFiles[$type]) === false) {
// When running in interactive mode, the reporter prints the full
// report many times, which will unlink the temp file. So we need
// to create a new one if it doesn't exist.
$this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs');
file_put_contents($this->tmpFiles[$type], '');
}
file_put_contents($this->tmpFiles[$type], $generatedReport, (FILE_APPEND | LOCK_EX));
} else {
file_put_contents($report['output'], $generatedReport, (FILE_APPEND | LOCK_EX));
}//end if
}//end foreach
if ($errorsShown === true || PHP_CODESNIFFER_CBF === true) {
$this->totalFiles++;
$this->totalErrors += $reportData['errors'];
$this->totalWarnings += $reportData['warnings'];
// When PHPCBF is running, we need to use the fixable error values
// after the report has run and fixed what it can.
if (PHP_CODESNIFFER_CBF === true) {
$this->totalFixable += $phpcsFile->getFixableCount();
$this->totalFixed += $phpcsFile->getFixedCount();
} else {
$this->totalFixable += $reportData['fixable'];
}
}
}//end cacheFileReport()
/**
* Generate summary information to be used during report generation.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed.
*
* @return array<string, string|int|array> Prepared report data.
* The format of prepared data is as follows:
* ```
* array(
* 'filename' => string The name of the current file.
* 'errors' => int The number of errors seen in the current file.
* 'warnings' => int The number of warnings seen in the current file.
* 'fixable' => int The number of fixable issues seen in the current file.
* 'messages' => array(
* int <Line number> => array(
* int <Column number> => array(
* int <Message index> => array(
* 'message' => string The error/warning message.
* 'source' => string The full error code for the message.
* 'severity' => int The severity of the message.
* 'fixable' => bool Whether this error/warning is auto-fixable.
* 'type' => string The type of message. Either 'ERROR' or 'WARNING'.
* )
* )
* )
* )
* )
* ```
*/
public function prepareFileReport(File $phpcsFile)
{
$report = [
'filename' => Common::stripBasepath($phpcsFile->getFilename(), $this->config->basepath),
'errors' => $phpcsFile->getErrorCount(),
'warnings' => $phpcsFile->getWarningCount(),
'fixable' => $phpcsFile->getFixableCount(),
'messages' => [],
];
if ($report['errors'] === 0 && $report['warnings'] === 0) {
// Perfect score!
return $report;
}
if ($this->config->recordErrors === false) {
$message = 'Errors are not being recorded but this report requires error messages. ';
$message .= 'This report will not show the correct information.';
$report['messages'][1][1] = [
[
'message' => $message,
'source' => 'Internal.RecordErrors',
'severity' => 5,
'fixable' => false,
'type' => 'ERROR',
],
];
return $report;
}
$errors = [];
// Merge errors and warnings.
foreach ($phpcsFile->getErrors() as $line => $lineErrors) {
foreach ($lineErrors as $column => $colErrors) {
$newErrors = [];
foreach ($colErrors as $data) {
$newErrors[] = [
'message' => $data['message'],
'source' => $data['source'],
'severity' => $data['severity'],
'fixable' => $data['fixable'],
'type' => 'ERROR',
];
}
$errors[$line][$column] = $newErrors;
}
ksort($errors[$line]);
}//end foreach
foreach ($phpcsFile->getWarnings() as $line => $lineWarnings) {
foreach ($lineWarnings as $column => $colWarnings) {
$newWarnings = [];
foreach ($colWarnings as $data) {
$newWarnings[] = [
'message' => $data['message'],
'source' => $data['source'],
'severity' => $data['severity'],
'fixable' => $data['fixable'],
'type' => 'WARNING',
];
}
if (isset($errors[$line]) === false) {
$errors[$line] = [];
}
if (isset($errors[$line][$column]) === true) {
$errors[$line][$column] = array_merge(
$newWarnings,
$errors[$line][$column]
);
} else {
$errors[$line][$column] = $newWarnings;
}
}//end foreach
ksort($errors[$line]);
}//end foreach
ksort($errors);
$report['messages'] = $errors;
return $report;
}//end prepareFileReport()
}//end class
@@ -0,0 +1,254 @@
<?php
/**
* CBF report for PHP_CodeSniffer.
*
* This report implements the various auto-fixing features of the
* PHPCBF script and is not intended (or allowed) to be selected as a
* report from the command line.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Exceptions\DeepExitException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Timing;
class Cbf implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
$errors = $phpcsFile->getFixableCount();
if ($errors !== 0) {
if (PHP_CODESNIFFER_VERBOSITY > 0) {
ob_end_clean();
$startTime = microtime(true);
echo "\t=> Fixing file: $errors/$errors violations remaining";
if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo PHP_EOL;
}
}
$fixed = $phpcsFile->fixer->fixFile();
}
if ($phpcsFile->config->stdin === true) {
// Replacing STDIN, so output current file to STDOUT
// even if nothing was fixed. Exit here because we
// can't process any more than 1 file in this setup.
$fixedContent = $phpcsFile->fixer->getContents();
throw new DeepExitException($fixedContent, 1);
}
if ($errors === 0) {
return false;
}
if (PHP_CODESNIFFER_VERBOSITY > 0) {
if ($fixed === false) {
echo 'ERROR';
} else {
echo 'DONE';
}
$timeTaken = ((microtime(true) - $startTime) * 1000);
if ($timeTaken < 1000) {
$timeTaken = round($timeTaken);
echo " in {$timeTaken}ms".PHP_EOL;
} else {
$timeTaken = round(($timeTaken / 1000), 2);
echo " in $timeTaken secs".PHP_EOL;
}
}
if ($fixed === true) {
// The filename in the report may be truncated due to a basepath setting
// but we are using it for writing here and not display,
// so find the correct path if basepath is in use.
$newFilename = $report['filename'].$phpcsFile->config->suffix;
if ($phpcsFile->config->basepath !== null) {
$newFilename = $phpcsFile->config->basepath.DIRECTORY_SEPARATOR.$newFilename;
}
$newContent = $phpcsFile->fixer->getContents();
file_put_contents($newFilename, $newContent);
if (PHP_CODESNIFFER_VERBOSITY > 0) {
if ($newFilename === $report['filename']) {
echo "\t=> File was overwritten".PHP_EOL;
} else {
echo "\t=> Fixed file written to ".basename($newFilename).PHP_EOL;
}
}
}
if (PHP_CODESNIFFER_VERBOSITY > 0) {
ob_start();
}
$errorCount = $phpcsFile->getErrorCount();
$warningCount = $phpcsFile->getWarningCount();
$fixableCount = $phpcsFile->getFixableCount();
$fixedCount = ($errors - $fixableCount);
echo $report['filename'].">>$errorCount>>$warningCount>>$fixableCount>>$fixedCount".PHP_EOL;
return $fixed;
}//end generateFileReport()
/**
* Prints a summary of fixed files.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
$lines = explode(PHP_EOL, $cachedData);
array_pop($lines);
if (empty($lines) === true) {
echo PHP_EOL.'No fixable errors were found'.PHP_EOL;
return;
}
$reportFiles = [];
$maxLength = 0;
$totalFixed = 0;
$failures = 0;
foreach ($lines as $line) {
$parts = explode('>>', $line);
$fileLen = strlen($parts[0]);
$reportFiles[$parts[0]] = [
'errors' => $parts[1],
'warnings' => $parts[2],
'fixable' => $parts[3],
'fixed' => $parts[4],
'strlen' => $fileLen,
];
$maxLength = max($maxLength, $fileLen);
$totalFixed += $parts[4];
if ($parts[3] > 0) {
$failures++;
}
}
$width = min($width, ($maxLength + 21));
$width = max($width, 70);
echo PHP_EOL."\033[1m".'PHPCBF RESULT SUMMARY'."\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
echo "\033[1m".'FILE'.str_repeat(' ', ($width - 20)).'FIXED REMAINING'."\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
foreach ($reportFiles as $file => $data) {
$padding = ($width - 18 - $data['strlen']);
if ($padding < 0) {
$file = '...'.substr($file, (($padding * -1) + 3));
$padding = 0;
}
echo $file.str_repeat(' ', $padding).' ';
if ($data['fixable'] > 0) {
echo "\033[31mFAILED TO FIX\033[0m".PHP_EOL;
continue;
}
$remaining = ($data['errors'] + $data['warnings']);
if ($data['fixed'] !== 0) {
echo $data['fixed'];
echo str_repeat(' ', (7 - strlen((string) $data['fixed'])));
} else {
echo '0 ';
}
if ($remaining !== 0) {
echo $remaining;
} else {
echo '0';
}
echo PHP_EOL;
}//end foreach
echo str_repeat('-', $width).PHP_EOL;
echo "\033[1mA TOTAL OF $totalFixed ERROR";
if ($totalFixed !== 1) {
echo 'S';
}
$numFiles = count($reportFiles);
echo ' WERE FIXED IN '.$numFiles.' FILE';
if ($numFiles !== 1) {
echo 'S';
}
echo "\033[0m";
if ($failures > 0) {
echo PHP_EOL.str_repeat('-', $width).PHP_EOL;
echo "\033[1mPHPCBF FAILED TO FIX $failures FILE";
if ($failures !== 1) {
echo 'S';
}
echo "\033[0m";
}
echo PHP_EOL.str_repeat('-', $width).PHP_EOL.PHP_EOL;
if ($toScreen === true && $interactive === false) {
Timing::printRunTime();
}
}//end generate()
}//end class
@@ -0,0 +1,111 @@
<?php
/**
* Checkstyle report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use XMLWriter;
class Checkstyle implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
$out = new XMLWriter;
$out->openMemory();
$out->setIndent(true);
if ($report['errors'] === 0 && $report['warnings'] === 0) {
// Nothing to print.
return false;
}
$out->startElement('file');
$out->writeAttribute('name', $report['filename']);
foreach ($report['messages'] as $line => $lineErrors) {
foreach ($lineErrors as $column => $colErrors) {
foreach ($colErrors as $error) {
$error['type'] = strtolower($error['type']);
if ($phpcsFile->config->encoding !== 'utf-8') {
$error['message'] = iconv($phpcsFile->config->encoding, 'utf-8', $error['message']);
}
$out->startElement('error');
$out->writeAttribute('line', $line);
$out->writeAttribute('column', $column);
$out->writeAttribute('severity', $error['type']);
$out->writeAttribute('message', $error['message']);
$out->writeAttribute('source', $error['source']);
$out->endElement();
}
}
}//end foreach
$out->endElement();
echo $out->flush();
return true;
}//end generateFileReport()
/**
* Prints all violations for processed files, in a Checkstyle format.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
echo '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
echo '<checkstyle version="'.Config::VERSION.'">'.PHP_EOL;
echo $cachedData;
echo '</checkstyle>'.PHP_EOL;
}//end generate()
}//end class
@@ -0,0 +1,365 @@
<?php
/**
* Full report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use Exception;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Common;
use PHP_CodeSniffer\Util\Timing;
class Code implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
if ($report['errors'] === 0 && $report['warnings'] === 0) {
// Nothing to print.
return false;
}
// How many lines to show above and below the error line.
$surroundingLines = 2;
$file = $report['filename'];
$tokens = $phpcsFile->getTokens();
if (empty($tokens) === true) {
if (PHP_CODESNIFFER_VERBOSITY === 1) {
$startTime = microtime(true);
echo 'CODE report is parsing '.basename($file).' ';
} else if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo "CODE report is forcing parse of $file".PHP_EOL;
}
try {
$phpcsFile->parse();
} catch (Exception $e) {
// This is a second parse, so ignore exceptions.
// They would have been added to the file's error list already.
}
if (PHP_CODESNIFFER_VERBOSITY === 1) {
$timeTaken = ((microtime(true) - $startTime) * 1000);
if ($timeTaken < 1000) {
$timeTaken = round($timeTaken);
echo "DONE in {$timeTaken}ms";
} else {
$timeTaken = round(($timeTaken / 1000), 2);
echo "DONE in $timeTaken secs";
}
echo PHP_EOL;
}
$tokens = $phpcsFile->getTokens();
}//end if
// Create an array that maps lines to the first token on the line.
$lineTokens = [];
$lastLine = 0;
$stackPtr = 0;
foreach ($tokens as $stackPtr => $token) {
if ($token['line'] !== $lastLine) {
if ($lastLine > 0) {
$lineTokens[$lastLine]['end'] = ($stackPtr - 1);
}
$lastLine++;
$lineTokens[$lastLine] = [
'start' => $stackPtr,
'end' => null,
];
}
}
// Make sure the last token in the file sits on an imaginary
// last line so it is easier to generate code snippets at the
// end of the file.
$lineTokens[$lastLine]['end'] = $stackPtr;
// Determine the longest code line we will be showing.
$maxSnippetLength = 0;
$eolLen = strlen($phpcsFile->eolChar);
foreach ($report['messages'] as $line => $lineErrors) {
$startLine = max(($line - $surroundingLines), 1);
$endLine = min(($line + $surroundingLines), $lastLine);
$maxLineNumLength = strlen($endLine);
for ($i = $startLine; $i <= $endLine; $i++) {
if ($i === 1) {
continue;
}
$lineLength = ($tokens[($lineTokens[$i]['start'] - 1)]['column'] + $tokens[($lineTokens[$i]['start'] - 1)]['length'] - $eolLen);
$maxSnippetLength = max($lineLength, $maxSnippetLength);
}
}
$maxSnippetLength += ($maxLineNumLength + 8);
// Determine the longest error message we will be showing.
$maxErrorLength = 0;
foreach ($report['messages'] as $lineErrors) {
foreach ($lineErrors as $colErrors) {
foreach ($colErrors as $error) {
$length = strlen($error['message']);
if ($showSources === true) {
$length += (strlen($error['source']) + 3);
}
$maxErrorLength = max($maxErrorLength, ($length + 1));
}
}
}
// The padding that all lines will require that are printing an error message overflow.
if ($report['warnings'] > 0) {
$typeLength = 7;
} else {
$typeLength = 5;
}
$errorPadding = str_repeat(' ', ($maxLineNumLength + 7));
$errorPadding .= str_repeat(' ', $typeLength);
$errorPadding .= ' ';
if ($report['fixable'] > 0) {
$errorPadding .= ' ';
}
$errorPaddingLength = strlen($errorPadding);
// The maximum amount of space an error message can use.
$maxErrorSpace = ($width - $errorPaddingLength);
if ($showSources === true) {
// Account for the chars used to print colors.
$maxErrorSpace += 8;
}
// Figure out the max report width we need and can use.
$fileLength = strlen($file);
$maxWidth = max(($fileLength + 6), ($maxErrorLength + $errorPaddingLength));
$width = max(min($width, $maxWidth), $maxSnippetLength);
if ($width < 70) {
$width = 70;
}
// Print the file header.
echo PHP_EOL."\033[1mFILE: ";
if ($fileLength <= ($width - 6)) {
echo $file;
} else {
echo '...'.substr($file, ($fileLength - ($width - 6)));
}
echo "\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
echo "\033[1m".'FOUND '.$report['errors'].' ERROR';
if ($report['errors'] !== 1) {
echo 'S';
}
if ($report['warnings'] > 0) {
echo ' AND '.$report['warnings'].' WARNING';
if ($report['warnings'] !== 1) {
echo 'S';
}
}
echo ' AFFECTING '.count($report['messages']).' LINE';
if (count($report['messages']) !== 1) {
echo 'S';
}
echo "\033[0m".PHP_EOL;
foreach ($report['messages'] as $line => $lineErrors) {
$startLine = max(($line - $surroundingLines), 1);
$endLine = min(($line + $surroundingLines), $lastLine);
$snippet = '';
if (isset($lineTokens[$startLine]) === true) {
for ($i = $lineTokens[$startLine]['start']; $i <= $lineTokens[$endLine]['end']; $i++) {
$snippetLine = $tokens[$i]['line'];
if ($lineTokens[$snippetLine]['start'] === $i) {
// Starting a new line.
if ($snippetLine === $line) {
$snippet .= "\033[1m".'>> ';
} else {
$snippet .= ' ';
}
$snippet .= str_repeat(' ', ($maxLineNumLength - strlen($snippetLine)));
$snippet .= $snippetLine.': ';
if ($snippetLine === $line) {
$snippet .= "\033[0m";
}
}
if (isset($tokens[$i]['orig_content']) === true) {
$tokenContent = $tokens[$i]['orig_content'];
} else {
$tokenContent = $tokens[$i]['content'];
}
if (strpos($tokenContent, "\t") !== false) {
$token = $tokens[$i];
$token['content'] = $tokenContent;
if (stripos(PHP_OS, 'WIN') === 0) {
$tab = "\000";
} else {
$tab = "\033[30;1m»\033[0m";
}
$phpcsFile->tokenizer->replaceTabsInToken($token, $tab, "\000");
$tokenContent = $token['content'];
}
$tokenContent = Common::prepareForOutput($tokenContent, ["\r", "\n", "\t"]);
$tokenContent = str_replace("\000", ' ', $tokenContent);
$underline = false;
if ($snippetLine === $line && isset($lineErrors[$tokens[$i]['column']]) === true) {
$underline = true;
}
// Underline invisible characters as well.
if ($underline === true && trim($tokenContent) === '') {
$snippet .= "\033[4m".' '."\033[0m".$tokenContent;
} else {
if ($underline === true) {
$snippet .= "\033[4m";
}
$snippet .= $tokenContent;
if ($underline === true) {
$snippet .= "\033[0m";
}
}
}//end for
}//end if
echo str_repeat('-', $width).PHP_EOL;
foreach ($lineErrors as $colErrors) {
foreach ($colErrors as $error) {
$padding = ($maxLineNumLength - strlen($line));
echo 'LINE '.str_repeat(' ', $padding).$line.': ';
if ($error['type'] === 'ERROR') {
echo "\033[31mERROR\033[0m";
if ($report['warnings'] > 0) {
echo ' ';
}
} else {
echo "\033[33mWARNING\033[0m";
}
echo ' ';
if ($report['fixable'] > 0) {
echo '[';
if ($error['fixable'] === true) {
echo 'x';
} else {
echo ' ';
}
echo '] ';
}
$message = $error['message'];
$message = str_replace("\n", "\n".$errorPadding, $message);
if ($showSources === true) {
$message = "\033[1m".$message."\033[0m".' ('.$error['source'].')';
}
$errorMsg = wordwrap(
$message,
$maxErrorSpace,
PHP_EOL.$errorPadding
);
echo $errorMsg.PHP_EOL;
}//end foreach
}//end foreach
echo str_repeat('-', $width).PHP_EOL;
echo rtrim($snippet).PHP_EOL;
}//end foreach
echo str_repeat('-', $width).PHP_EOL;
if ($report['fixable'] > 0) {
echo "\033[1m".'PHPCBF CAN FIX THE '.$report['fixable'].' MARKED SNIFF VIOLATIONS AUTOMATICALLY'."\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
}
return true;
}//end generateFileReport()
/**
* Prints all errors and warnings for each file processed.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
if ($cachedData === '') {
return;
}
echo $cachedData;
if ($toScreen === true && $interactive === false) {
Timing::printRunTime();
}
}//end generate()
}//end class
@@ -0,0 +1,92 @@
<?php
/**
* CSV report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
class Csv implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
if ($report['errors'] === 0 && $report['warnings'] === 0) {
// Nothing to print.
return false;
}
foreach ($report['messages'] as $line => $lineErrors) {
foreach ($lineErrors as $column => $colErrors) {
foreach ($colErrors as $error) {
$filename = str_replace('"', '\"', $report['filename']);
$message = str_replace('"', '\"', $error['message']);
$type = strtolower($error['type']);
$source = $error['source'];
$severity = $error['severity'];
$fixable = (int) $error['fixable'];
echo "\"$filename\",$line,$column,$type,\"$message\",$source,$severity,$fixable".PHP_EOL;
}
}
}
return true;
}//end generateFileReport()
/**
* Generates a csv report.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
echo 'File,Line,Column,Type,Message,Source,Severity,Fixable'.PHP_EOL;
echo $cachedData;
}//end generate()
}//end class
@@ -0,0 +1,131 @@
<?php
/**
* Diff report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
class Diff implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
$errors = $phpcsFile->getFixableCount();
if ($errors === 0) {
return false;
}
$phpcsFile->disableCaching();
$tokens = $phpcsFile->getTokens();
if (empty($tokens) === true) {
if (PHP_CODESNIFFER_VERBOSITY === 1) {
$startTime = microtime(true);
echo 'DIFF report is parsing '.basename($report['filename']).' ';
} else if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo 'DIFF report is forcing parse of '.$report['filename'].PHP_EOL;
}
$phpcsFile->parse();
if (PHP_CODESNIFFER_VERBOSITY === 1) {
$timeTaken = ((microtime(true) - $startTime) * 1000);
if ($timeTaken < 1000) {
$timeTaken = round($timeTaken);
echo "DONE in {$timeTaken}ms";
} else {
$timeTaken = round(($timeTaken / 1000), 2);
echo "DONE in $timeTaken secs";
}
echo PHP_EOL;
}
$phpcsFile->fixer->startFile($phpcsFile);
}//end if
if (PHP_CODESNIFFER_VERBOSITY > 1) {
ob_end_clean();
echo "\t*** START FILE FIXING ***".PHP_EOL;
}
$fixed = $phpcsFile->fixer->fixFile();
if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo "\t*** END FILE FIXING ***".PHP_EOL;
ob_start();
}
if ($fixed === false) {
return false;
}
$diff = $phpcsFile->fixer->generateDiff();
if ($diff === '') {
// Nothing to print.
return false;
}
echo $diff.PHP_EOL;
return true;
}//end generateFileReport()
/**
* Prints all errors and warnings for each file processed.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
echo $cachedData;
if ($toScreen === true && $cachedData !== '') {
echo PHP_EOL;
}
}//end generate()
}//end class
@@ -0,0 +1,91 @@
<?php
/**
* Emacs report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
class Emacs implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
if ($report['errors'] === 0 && $report['warnings'] === 0) {
// Nothing to print.
return false;
}
foreach ($report['messages'] as $line => $lineErrors) {
foreach ($lineErrors as $column => $colErrors) {
foreach ($colErrors as $error) {
$message = $error['message'];
if ($showSources === true) {
$message .= ' ('.$error['source'].')';
}
$type = strtolower($error['type']);
echo $report['filename'].':'.$line.':'.$column.': '.$type.' - '.$message.PHP_EOL;
}
}
}
return true;
}//end generateFileReport()
/**
* Generates an emacs report.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
echo $cachedData;
}//end generate()
}//end class
@@ -0,0 +1,260 @@
<?php
/**
* Full report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Timing;
class Full implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
if ($report['errors'] === 0 && $report['warnings'] === 0) {
// Nothing to print.
return false;
}
// The length of the word ERROR or WARNING; used for padding.
if ($report['warnings'] > 0) {
$typeLength = 7;
} else {
$typeLength = 5;
}
// Work out the max line number length for formatting.
$maxLineNumLength = max(array_map('strlen', array_keys($report['messages'])));
// The padding that all lines will require that are
// printing an error message overflow.
$paddingLine2 = str_repeat(' ', ($maxLineNumLength + 1));
$paddingLine2 .= ' | ';
$paddingLine2 .= str_repeat(' ', $typeLength);
$paddingLine2 .= ' | ';
if ($report['fixable'] > 0) {
$paddingLine2 .= ' ';
}
$paddingLength = strlen($paddingLine2);
// Make sure the report width isn't too big.
$maxErrorLength = 0;
foreach ($report['messages'] as $lineErrors) {
foreach ($lineErrors as $colErrors) {
foreach ($colErrors as $error) {
// Start with the presumption of a single line error message.
$length = strlen($error['message']);
$srcLength = (strlen($error['source']) + 3);
if ($showSources === true) {
$length += $srcLength;
}
// ... but also handle multi-line messages correctly.
if (strpos($error['message'], "\n") !== false) {
$errorLines = explode("\n", $error['message']);
$length = max(array_map('strlen', $errorLines));
if ($showSources === true) {
$lastLine = array_pop($errorLines);
$length = max($length, (strlen($lastLine) + $srcLength));
}
}
$maxErrorLength = max($maxErrorLength, ($length + 1));
}//end foreach
}//end foreach
}//end foreach
$file = $report['filename'];
$fileLength = strlen($file);
$maxWidth = max(($fileLength + 6), ($maxErrorLength + $paddingLength));
$width = min($width, $maxWidth);
if ($width < 70) {
$width = 70;
}
echo PHP_EOL."\033[1mFILE: ";
if ($fileLength <= ($width - 6)) {
echo $file;
} else {
echo '...'.substr($file, ($fileLength - ($width - 6)));
}
echo "\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
echo "\033[1m".'FOUND '.$report['errors'].' ERROR';
if ($report['errors'] !== 1) {
echo 'S';
}
if ($report['warnings'] > 0) {
echo ' AND '.$report['warnings'].' WARNING';
if ($report['warnings'] !== 1) {
echo 'S';
}
}
echo ' AFFECTING '.count($report['messages']).' LINE';
if (count($report['messages']) !== 1) {
echo 'S';
}
echo "\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
// The maximum amount of space an error message can use.
$maxErrorSpace = ($width - $paddingLength - 1);
$beforeMsg = '';
$afterMsg = '';
if ($showSources === true) {
$beforeMsg = "\033[1m";
$afterMsg = "\033[0m";
}
$beforeAfterLength = strlen($beforeMsg.$afterMsg);
foreach ($report['messages'] as $line => $lineErrors) {
foreach ($lineErrors as $colErrors) {
foreach ($colErrors as $error) {
$errorMsg = wordwrap(
$error['message'],
$maxErrorSpace
);
// Add the padding _after_ the wordwrap as the message itself may contain line breaks
// and those lines will also need to receive padding.
$errorMsg = str_replace("\n", $afterMsg.PHP_EOL.$paddingLine2.$beforeMsg, $errorMsg);
$errorMsg = $beforeMsg.$errorMsg.$afterMsg;
if ($showSources === true) {
$lastMsg = $errorMsg;
$startPosLastLine = strrpos($errorMsg, PHP_EOL.$paddingLine2.$beforeMsg);
if ($startPosLastLine !== false) {
// Message is multiline. Grab the text of last line of the message, including the color codes.
$lastMsg = substr($errorMsg, ($startPosLastLine + strlen(PHP_EOL.$paddingLine2)));
}
// When show sources is used, the message itself will be bolded, so we need to correct the length.
$sourceSuffix = '('.$error['source'].')';
$lastMsgPlusSourceLength = strlen($lastMsg);
// Add space + source suffix length.
$lastMsgPlusSourceLength += (1 + strlen($sourceSuffix));
// Correct for the color codes.
$lastMsgPlusSourceLength -= $beforeAfterLength;
if ($lastMsgPlusSourceLength > $maxErrorSpace) {
$errorMsg .= PHP_EOL.$paddingLine2.$sourceSuffix;
} else {
$errorMsg .= ' '.$sourceSuffix;
}
}//end if
// The padding that goes on the front of the line.
$padding = ($maxLineNumLength - strlen($line));
echo ' '.str_repeat(' ', $padding).$line.' | ';
if ($error['type'] === 'ERROR') {
echo "\033[31mERROR\033[0m";
if ($report['warnings'] > 0) {
echo ' ';
}
} else {
echo "\033[33mWARNING\033[0m";
}
echo ' | ';
if ($report['fixable'] > 0) {
echo '[';
if ($error['fixable'] === true) {
echo 'x';
} else {
echo ' ';
}
echo '] ';
}
echo $errorMsg.PHP_EOL;
}//end foreach
}//end foreach
}//end foreach
echo str_repeat('-', $width).PHP_EOL;
if ($report['fixable'] > 0) {
echo "\033[1m".'PHPCBF CAN FIX THE '.$report['fixable'].' MARKED SNIFF VIOLATIONS AUTOMATICALLY'."\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
}
echo PHP_EOL;
return true;
}//end generateFileReport()
/**
* Prints all errors and warnings for each file processed.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
if ($cachedData === '') {
return;
}
echo $cachedData;
if ($toScreen === true && $interactive === false) {
Timing::printRunTime();
}
}//end generate()
}//end class
@@ -0,0 +1,173 @@
<?php
/**
* Info report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Timing;
class Info implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
$metrics = $phpcsFile->getMetrics();
foreach ($metrics as $metric => $data) {
foreach ($data['values'] as $value => $count) {
echo "$metric>>$value>>$count".PHP_EOL;
}
}
return true;
}//end generateFileReport()
/**
* Prints the recorded metrics.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
$lines = explode(PHP_EOL, $cachedData);
array_pop($lines);
if (empty($lines) === true) {
return;
}
$metrics = [];
foreach ($lines as $line) {
$parts = explode('>>', $line);
$metric = $parts[0];
$value = $parts[1];
$count = $parts[2];
if (isset($metrics[$metric]) === false) {
$metrics[$metric] = [];
}
if (isset($metrics[$metric][$value]) === false) {
$metrics[$metric][$value] = $count;
} else {
$metrics[$metric][$value] += $count;
}
}
ksort($metrics);
echo PHP_EOL."\033[1m".'PHP CODE SNIFFER INFORMATION REPORT'."\033[0m".PHP_EOL;
echo str_repeat('-', 70).PHP_EOL;
foreach ($metrics as $metric => $values) {
if (count($values) === 1) {
$count = reset($values);
$value = key($values);
echo "$metric: \033[4m$value\033[0m [$count/$count, 100%]".PHP_EOL;
} else {
$totalCount = 0;
$valueWidth = 0;
foreach ($values as $value => $count) {
$totalCount += $count;
$valueWidth = max($valueWidth, strlen($value));
}
// Length of the total string, plus however many
// thousands separators there are.
$countWidth = strlen($totalCount);
$thousandSeparatorCount = floor($countWidth / 3);
$countWidth += $thousandSeparatorCount;
// Account for 'total' line.
$valueWidth = max(5, $valueWidth);
echo "$metric:".PHP_EOL;
ksort($values, SORT_NATURAL);
arsort($values);
$percentPrefixWidth = 0;
$percentWidth = 6;
foreach ($values as $value => $count) {
$percent = round(($count / $totalCount * 100), 2);
$percentPrefix = '';
if ($percent === 0.00) {
$percent = 0.01;
$percentPrefix = '<';
$percentPrefixWidth = 2;
$percentWidth = 4;
}
printf(
"\t%-{$valueWidth}s => %{$countWidth}s (%{$percentPrefixWidth}s%{$percentWidth}.2f%%)".PHP_EOL,
$value,
number_format($count),
$percentPrefix,
$percent
);
}
echo "\t".str_repeat('-', ($valueWidth + $countWidth + 15)).PHP_EOL;
printf(
"\t%-{$valueWidth}s => %{$countWidth}s (100.00%%)".PHP_EOL,
'total',
number_format($totalCount)
);
}//end if
echo PHP_EOL;
}//end foreach
echo str_repeat('-', 70).PHP_EOL;
if ($toScreen === true && $interactive === false) {
Timing::printRunTime();
}
}//end generate()
}//end class
@@ -0,0 +1,107 @@
<?php
/**
* JSON report for PHP_CodeSniffer.
*
* @author Jeffrey Fisher <jeffslofish@gmail.com>
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
class Json implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
$filename = str_replace('\\', '\\\\', $report['filename']);
$filename = str_replace('"', '\"', $filename);
$filename = str_replace('/', '\/', $filename);
echo '"'.$filename.'":{';
echo '"errors":'.$report['errors'].',"warnings":'.$report['warnings'].',"messages":[';
$messages = '';
foreach ($report['messages'] as $line => $lineErrors) {
foreach ($lineErrors as $column => $colErrors) {
foreach ($colErrors as $error) {
$error['message'] = str_replace("\n", '\n', $error['message']);
$error['message'] = str_replace("\r", '\r', $error['message']);
$error['message'] = str_replace("\t", '\t', $error['message']);
$fixable = false;
if ($error['fixable'] === true) {
$fixable = true;
}
$messagesObject = (object) $error;
$messagesObject->line = $line;
$messagesObject->column = $column;
$messagesObject->fixable = $fixable;
$messages .= json_encode($messagesObject).",";
}
}
}//end foreach
echo rtrim($messages, ',');
echo ']},';
return true;
}//end generateFileReport()
/**
* Generates a JSON report.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
echo '{"totals":{"errors":'.$totalErrors.',"warnings":'.$totalWarnings.',"fixable":'.$totalFixable.'},"files":{';
echo rtrim($cachedData, ',');
echo "}}".PHP_EOL;
}//end generate()
}//end class
@@ -0,0 +1,133 @@
<?php
/**
* JUnit report for PHP_CodeSniffer.
*
* @author Oleg Lobach <oleg@lobach.info>
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use XMLWriter;
class Junit implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
$out = new XMLWriter;
$out->openMemory();
$out->setIndent(true);
$out->startElement('testsuite');
$out->writeAttribute('name', $report['filename']);
$out->writeAttribute('errors', 0);
if (count($report['messages']) === 0) {
$out->writeAttribute('tests', 1);
$out->writeAttribute('failures', 0);
$out->startElement('testcase');
$out->writeAttribute('name', $report['filename']);
$out->endElement();
} else {
$failures = ($report['errors'] + $report['warnings']);
$out->writeAttribute('tests', $failures);
$out->writeAttribute('failures', $failures);
foreach ($report['messages'] as $line => $lineErrors) {
foreach ($lineErrors as $column => $colErrors) {
foreach ($colErrors as $error) {
$out->startElement('testcase');
$out->writeAttribute('name', $error['source'].' at '.$report['filename']." ($line:$column)");
$error['type'] = strtolower($error['type']);
if ($phpcsFile->config->encoding !== 'utf-8') {
$error['message'] = iconv($phpcsFile->config->encoding, 'utf-8', $error['message']);
}
$out->startElement('failure');
$out->writeAttribute('type', $error['type']);
$out->writeAttribute('message', $error['message']);
$out->endElement();
$out->endElement();
}
}
}
}//end if
$out->endElement();
echo $out->flush();
return true;
}//end generateFileReport()
/**
* Prints all violations for processed files, in a proprietary XML format.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
// Figure out the total number of tests.
$tests = 0;
$matches = [];
preg_match_all('/tests="([0-9]+)"/', $cachedData, $matches);
if (isset($matches[1]) === true) {
foreach ($matches[1] as $match) {
$tests += $match;
}
}
$failures = ($totalErrors + $totalWarnings);
echo '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
echo '<testsuites name="PHP_CodeSniffer '.Config::VERSION.'" errors="0" tests="'.$tests.'" failures="'.$failures.'">'.PHP_EOL;
echo $cachedData;
echo '</testsuites>'.PHP_EOL;
}//end generate()
}//end class
@@ -0,0 +1,243 @@
<?php
/**
* Notify-send report for PHP_CodeSniffer.
*
* Supported configuration parameters:
* - notifysend_path - Full path to notify-send cli command
* - notifysend_timeout - Timeout in milliseconds
* - notifysend_showok - Show "ok, all fine" messages (0/1)
*
* @author Christian Weiske <christian.weiske@netresearch.de>
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2012-2014 Christian Weiske
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Common;
class Notifysend implements Report
{
/**
* Notification timeout in milliseconds.
*
* @var integer
*/
protected $timeout = 3000;
/**
* Path to notify-send command.
*
* @var string
*/
protected $path = 'notify-send';
/**
* Show "ok, all fine" messages.
*
* @var boolean
*/
protected $showOk = true;
/**
* Version of installed notify-send executable.
*
* @var string
*/
protected $version = null;
/**
* Load configuration data.
*/
public function __construct()
{
$path = Config::getExecutablePath('notifysend');
if ($path !== null) {
$this->path = Common::escapeshellcmd($path);
}
$timeout = Config::getConfigData('notifysend_timeout');
if ($timeout !== null) {
$this->timeout = (int) $timeout;
}
$showOk = Config::getConfigData('notifysend_showok');
if ($showOk !== null) {
$this->showOk = (bool) $showOk;
}
$this->version = str_replace(
'notify-send ',
'',
exec($this->path.' --version')
);
}//end __construct()
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
echo $report['filename'].PHP_EOL;
// We want this file counted in the total number
// of checked files even if it has no errors.
return true;
}//end generateFileReport()
/**
* Generates a summary of errors and warnings for each file processed.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
$checkedFiles = explode(PHP_EOL, trim($cachedData));
$msg = $this->generateMessage($checkedFiles, $totalErrors, $totalWarnings);
if ($msg === null) {
if ($this->showOk === true) {
$this->notifyAllFine();
}
} else {
$this->notifyErrors($msg);
}
}//end generate()
/**
* Generate the error message to show to the user.
*
* @param string[] $checkedFiles The files checked during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
*
* @return string|null Error message or NULL if no error/warning found.
*/
protected function generateMessage($checkedFiles, $totalErrors, $totalWarnings)
{
if ($totalErrors === 0 && $totalWarnings === 0) {
// Nothing to print.
return null;
}
$totalFiles = count($checkedFiles);
$msg = '';
if ($totalFiles > 1) {
$msg .= 'Checked '.$totalFiles.' files'.PHP_EOL;
} else {
$msg .= $checkedFiles[0].PHP_EOL;
}
if ($totalWarnings > 0) {
$msg .= $totalWarnings.' warnings'.PHP_EOL;
}
if ($totalErrors > 0) {
$msg .= $totalErrors.' errors'.PHP_EOL;
}
return $msg;
}//end generateMessage()
/**
* Tell the user that all is fine and no error/warning has been found.
*
* @return void
*/
protected function notifyAllFine()
{
$cmd = $this->getBasicCommand();
$cmd .= ' -i info';
$cmd .= ' "PHP CodeSniffer: Ok"';
$cmd .= ' "All fine"';
exec($cmd);
}//end notifyAllFine()
/**
* Tell the user that errors/warnings have been found.
*
* @param string $msg Message to display.
*
* @return void
*/
protected function notifyErrors($msg)
{
$cmd = $this->getBasicCommand();
$cmd .= ' -i error';
$cmd .= ' "PHP CodeSniffer: Error"';
$cmd .= ' '.escapeshellarg(trim($msg));
exec($cmd);
}//end notifyErrors()
/**
* Generate and return the basic notify-send command string to execute.
*
* @return string Shell command with common parameters.
*/
protected function getBasicCommand()
{
$cmd = $this->path;
$cmd .= ' --category dev.validate';
$cmd .= ' -h int:transient:1';
$cmd .= ' -t '.(int) $this->timeout;
if (version_compare($this->version, '0.7.3', '>=') === true) {
$cmd .= ' -a phpcs';
}
return $cmd;
}//end getBasicCommand()
}//end class
@@ -0,0 +1,161 @@
<?php
/**
* Performance report for PHP_CodeSniffer.
*
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2023 Juliette Reinders Folmer. All rights reserved.
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Common;
use PHP_CodeSniffer\Util\Timing;
class Performance implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
$times = $phpcsFile->getListenerTimes();
foreach ($times as $sniff => $time) {
echo "$sniff>>$time".PHP_EOL;
}
return true;
}//end generateFileReport()
/**
* Prints the sniff performance report.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
$lines = explode(PHP_EOL, $cachedData);
array_pop($lines);
if (empty($lines) === true) {
return;
}
// First collect the accumulated timings.
$timings = [];
$totalSniffTime = 0;
foreach ($lines as $line) {
$parts = explode('>>', $line);
$sniffClass = $parts[0];
$time = $parts[1];
if (isset($timings[$sniffClass]) === false) {
$timings[$sniffClass] = 0;
}
$timings[$sniffClass] += $time;
$totalSniffTime += $time;
}
// Next, tidy up the sniff names and determine max needed column width.
$totalTimes = [];
$maxNameWidth = 0;
foreach ($timings as $sniffClass => $secs) {
$sniffCode = Common::getSniffCode($sniffClass);
$maxNameWidth = max($maxNameWidth, strlen($sniffCode));
$totalTimes[$sniffCode] = $secs;
}
// Leading space + up to 12 chars for the number.
$maxTimeWidth = 13;
// Leading space, open parenthesis, up to 5 chars for the number, space + % and close parenthesis.
$maxPercWidth = 10;
// Calculate the maximum width available for the sniff name.
$maxNameWidth = min(($width - $maxTimeWidth - $maxPercWidth), max(($width - $maxTimeWidth - $maxPercWidth), $maxNameWidth));
arsort($totalTimes);
echo PHP_EOL."\033[1m".'PHP CODE SNIFFER SNIFF PERFORMANCE REPORT'."\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
echo "\033[1m".'SNIFF'.str_repeat(' ', ($width - 31)).'TIME TAKEN (SECS) (%)'."\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
// Mark sniffs which take more than twice as long as the average processing time per sniff
// in orange and when they take more than three times as long as the average,
// mark them in red.
$avgSniffTime = ($totalSniffTime / count($totalTimes));
$doubleAvgSniffTime = (2 * $avgSniffTime);
$tripleAvgSniffTime = (3 * $avgSniffTime);
$format = "%- {$maxNameWidth}.{$maxNameWidth}s % 12.6f (% 5.1f %%)".PHP_EOL;
$formatBold = "\033[1m%- {$maxNameWidth}.{$maxNameWidth}s % 12.6f (% 5.1f %%)\033[0m".PHP_EOL;
$formatWarning = "%- {$maxNameWidth}.{$maxNameWidth}s \033[33m% 12.6f (% 5.1f %%)\033[0m".PHP_EOL;
$formatError = "%- {$maxNameWidth}.{$maxNameWidth}s \033[31m% 12.6f (% 5.1f %%)\033[0m".PHP_EOL;
foreach ($totalTimes as $sniff => $time) {
$percent = round((($time / $totalSniffTime) * 100), 1);
if ($time > $tripleAvgSniffTime) {
printf($formatError, $sniff, $time, $percent);
} else if ($time > $doubleAvgSniffTime) {
printf($formatWarning, $sniff, $time, $percent);
} else {
printf($format, $sniff, $time, $percent);
}
}
echo str_repeat('-', $width).PHP_EOL;
printf($formatBold, 'TOTAL SNIFF PROCESSING TIME', $totalSniffTime, 100);
$runTime = (Timing::getDuration() / 1000);
$phpcsTime = ($runTime - $totalSniffTime);
echo PHP_EOL.str_repeat('-', $width).PHP_EOL;
printf($format, 'Time taken by sniffs', $totalSniffTime, round((($totalSniffTime / $runTime) * 100), 1));
printf($format, 'Time taken by PHPCS runner', $phpcsTime, round((($phpcsTime / $runTime) * 100), 1));
echo str_repeat('-', $width).PHP_EOL;
printf($formatBold, 'TOTAL RUN TIME', $runTime, 100);
echo str_repeat('-', $width).PHP_EOL;
}//end generate()
}//end class
@@ -0,0 +1,87 @@
<?php
/**
* An interface that PHP_CodeSniffer reports must implement.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
interface Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* The format of the `$report` parameter the function receives is as follows:
* ```
* array(
* 'filename' => string The name of the current file.
* 'errors' => int The number of errors seen in the current file.
* 'warnings' => int The number of warnings seen in the current file.
* 'fixable' => int The number of fixable issues seen in the current file.
* 'messages' => array(
* int <Line number> => array(
* int <Column number> => array(
* int <Message index> => array(
* 'message' => string The error/warning message.
* 'source' => string The full error code for the message.
* 'severity' => int The severity of the message.
* 'fixable' => bool Whether this error/warning is auto-fixable.
* 'type' => string The type of message. Either 'ERROR' or 'WARNING'.
* )
* )
* )
* )
* )
* ```
*
* @param array<string, string|int|array> $report Prepared report data.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80);
/**
* Generate the actual report.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
);
}//end interface
@@ -0,0 +1,337 @@
<?php
/**
* Source report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Timing;
class Source implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
if ($report['errors'] === 0 && $report['warnings'] === 0) {
// Nothing to print.
return false;
}
$sources = [];
foreach ($report['messages'] as $lineErrors) {
foreach ($lineErrors as $colErrors) {
foreach ($colErrors as $error) {
$src = $error['source'];
if (isset($sources[$src]) === false) {
$sources[$src] = [
'fixable' => (int) $error['fixable'],
'count' => 1,
];
} else {
$sources[$src]['count']++;
}
}
}
}
foreach ($sources as $source => $data) {
echo $source.'>>'.$data['fixable'].'>>'.$data['count'].PHP_EOL;
}
return true;
}//end generateFileReport()
/**
* Prints the source of all errors and warnings.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
$lines = explode(PHP_EOL, $cachedData);
array_pop($lines);
if (empty($lines) === true) {
return;
}
$sources = [];
$maxLength = 0;
foreach ($lines as $line) {
$parts = explode('>>', $line);
$source = $parts[0];
$fixable = (bool) $parts[1];
$count = $parts[2];
if (isset($sources[$source]) === false) {
if ($showSources === true) {
$parts = null;
$sniff = $source;
} else {
$parts = explode('.', $source);
if ($parts[0] === 'Internal') {
$parts[2] = $parts[1];
$parts[1] = '';
}
$parts[1] = $this->makeFriendlyName($parts[1]);
$sniff = $this->makeFriendlyName($parts[2]);
if (isset($parts[3]) === true) {
$name = $this->makeFriendlyName($parts[3]);
$name[0] = strtolower($name[0]);
$sniff .= ' '.$name;
unset($parts[3]);
}
$parts[2] = $sniff;
}//end if
$maxLength = max($maxLength, strlen($sniff));
$sources[$source] = [
'count' => $count,
'fixable' => $fixable,
'parts' => $parts,
];
} else {
$sources[$source]['count'] += $count;
}//end if
}//end foreach
if ($showSources === true) {
$width = min($width, ($maxLength + 11));
} else {
$width = min($width, ($maxLength + 41));
}
$width = max($width, 70);
// Sort the data based on counts and source code.
$sourceCodes = array_keys($sources);
$counts = [];
foreach ($sources as $source => $data) {
$counts[$source] = $data['count'];
}
array_multisort($counts, SORT_DESC, $sourceCodes, SORT_ASC, SORT_NATURAL, $sources);
echo PHP_EOL."\033[1mPHP CODE SNIFFER VIOLATION SOURCE SUMMARY\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL."\033[1m";
if ($showSources === true) {
if ($totalFixable > 0) {
echo ' SOURCE'.str_repeat(' ', ($width - 15)).'COUNT'.PHP_EOL;
} else {
echo 'SOURCE'.str_repeat(' ', ($width - 11)).'COUNT'.PHP_EOL;
}
} else {
if ($totalFixable > 0) {
echo ' STANDARD CATEGORY SNIFF'.str_repeat(' ', ($width - 44)).'COUNT'.PHP_EOL;
} else {
echo 'STANDARD CATEGORY SNIFF'.str_repeat(' ', ($width - 40)).'COUNT'.PHP_EOL;
}
}
echo "\033[0m".str_repeat('-', $width).PHP_EOL;
$fixableSources = 0;
if ($showSources === true) {
$maxSniffWidth = ($width - 7);
} else {
$maxSniffWidth = ($width - 37);
}
if ($totalFixable > 0) {
$maxSniffWidth -= 4;
}
foreach ($sources as $source => $sourceData) {
if ($totalFixable > 0) {
echo '[';
if ($sourceData['fixable'] === true) {
echo 'x';
$fixableSources++;
} else {
echo ' ';
}
echo '] ';
}
if ($showSources === true) {
if (strlen($source) > $maxSniffWidth) {
$source = substr($source, 0, $maxSniffWidth);
}
echo $source;
if ($totalFixable > 0) {
echo str_repeat(' ', ($width - 9 - strlen($source)));
} else {
echo str_repeat(' ', ($width - 5 - strlen($source)));
}
} else {
$parts = $sourceData['parts'];
if (strlen($parts[0]) > 8) {
$parts[0] = substr($parts[0], 0, ((strlen($parts[0]) - 8) * -1));
}
echo $parts[0].str_repeat(' ', (10 - strlen($parts[0])));
$category = $parts[1];
if (strlen($category) > 18) {
$category = substr($category, 0, ((strlen($category) - 18) * -1));
}
echo $category.str_repeat(' ', (20 - strlen($category)));
$sniff = $parts[2];
if (strlen($sniff) > $maxSniffWidth) {
$sniff = substr($sniff, 0, $maxSniffWidth);
}
if ($totalFixable > 0) {
echo $sniff.str_repeat(' ', ($width - 39 - strlen($sniff)));
} else {
echo $sniff.str_repeat(' ', ($width - 35 - strlen($sniff)));
}
}//end if
echo $sourceData['count'].PHP_EOL;
}//end foreach
echo str_repeat('-', $width).PHP_EOL;
echo "\033[1m".'A TOTAL OF '.($totalErrors + $totalWarnings).' SNIFF VIOLATION';
if (($totalErrors + $totalWarnings) > 1) {
echo 'S';
}
echo ' WERE FOUND IN '.count($sources).' SOURCE';
if (count($sources) !== 1) {
echo 'S';
}
echo "\033[0m";
if ($totalFixable > 0) {
echo PHP_EOL.str_repeat('-', $width).PHP_EOL;
echo "\033[1mPHPCBF CAN FIX THE $fixableSources MARKED SOURCES AUTOMATICALLY ($totalFixable VIOLATIONS IN TOTAL)\033[0m";
}
echo PHP_EOL.str_repeat('-', $width).PHP_EOL.PHP_EOL;
if ($toScreen === true && $interactive === false) {
Timing::printRunTime();
}
}//end generate()
/**
* Converts a camel caps name into a readable string.
*
* @param string $name The camel caps name to convert.
*
* @return string
*/
public function makeFriendlyName($name)
{
if (trim($name) === '') {
return '';
}
$friendlyName = '';
$length = strlen($name);
$lastWasUpper = false;
$lastWasNumeric = false;
for ($i = 0; $i < $length; $i++) {
if (is_numeric($name[$i]) === true) {
if ($lastWasNumeric === false) {
$friendlyName .= ' ';
}
$lastWasUpper = false;
$lastWasNumeric = true;
} else {
$lastWasNumeric = false;
$char = strtolower($name[$i]);
if ($char === $name[$i]) {
// Lowercase.
$lastWasUpper = false;
} else {
// Uppercase.
if ($lastWasUpper === false) {
$friendlyName .= ' ';
if ($i < ($length - 1)) {
$next = $name[($i + 1)];
if (strtolower($next) === $next) {
// Next char is lowercase so it is a word boundary.
$name[$i] = strtolower($name[$i]);
}
}
}
$lastWasUpper = true;
}
}//end if
$friendlyName .= $name[$i];
}//end for
$friendlyName = trim($friendlyName);
$friendlyName[0] = strtoupper($friendlyName[0]);
return $friendlyName;
}//end makeFriendlyName()
}//end class
@@ -0,0 +1,184 @@
<?php
/**
* Summary report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Timing;
class Summary implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
if (PHP_CODESNIFFER_VERBOSITY === 0
&& $report['errors'] === 0
&& $report['warnings'] === 0
) {
// Nothing to print.
return false;
}
echo $report['filename'].'>>'.$report['errors'].'>>'.$report['warnings'].PHP_EOL;
return true;
}//end generateFileReport()
/**
* Generates a summary of errors and warnings for each file processed.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
$lines = explode(PHP_EOL, $cachedData);
array_pop($lines);
if (empty($lines) === true) {
return;
}
$reportFiles = [];
$maxLength = 0;
foreach ($lines as $line) {
$parts = explode('>>', $line);
$fileLen = strlen($parts[0]);
$reportFiles[$parts[0]] = [
'errors' => $parts[1],
'warnings' => $parts[2],
'strlen' => $fileLen,
];
$maxLength = max($maxLength, $fileLen);
}
uksort(
$reportFiles,
function ($keyA, $keyB) {
$pathPartsA = explode(DIRECTORY_SEPARATOR, $keyA);
$pathPartsB = explode(DIRECTORY_SEPARATOR, $keyB);
do {
$partA = array_shift($pathPartsA);
$partB = array_shift($pathPartsB);
} while ($partA === $partB && empty($pathPartsA) === false && empty($pathPartsB) === false);
if (empty($pathPartsA) === false && empty($pathPartsB) === true) {
return 1;
} else if (empty($pathPartsA) === true && empty($pathPartsB) === false) {
return -1;
} else {
return strcasecmp($partA, $partB);
}
}
);
$width = min($width, ($maxLength + 21));
$width = max($width, 70);
echo PHP_EOL."\033[1m".'PHP CODE SNIFFER REPORT SUMMARY'."\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
echo "\033[1m".'FILE'.str_repeat(' ', ($width - 20)).'ERRORS WARNINGS'."\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
foreach ($reportFiles as $file => $data) {
$padding = ($width - 18 - $data['strlen']);
if ($padding < 0) {
$file = '...'.substr($file, (($padding * -1) + 3));
$padding = 0;
}
echo $file.str_repeat(' ', $padding).' ';
if ($data['errors'] !== 0) {
echo "\033[31m".$data['errors']."\033[0m";
echo str_repeat(' ', (8 - strlen((string) $data['errors'])));
} else {
echo '0 ';
}
if ($data['warnings'] !== 0) {
echo "\033[33m".$data['warnings']."\033[0m";
} else {
echo '0';
}
echo PHP_EOL;
}//end foreach
echo str_repeat('-', $width).PHP_EOL;
echo "\033[1mA TOTAL OF $totalErrors ERROR";
if ($totalErrors !== 1) {
echo 'S';
}
echo ' AND '.$totalWarnings.' WARNING';
if ($totalWarnings !== 1) {
echo 'S';
}
echo ' WERE FOUND IN '.$totalFiles.' FILE';
if ($totalFiles !== 1) {
echo 'S';
}
echo "\033[0m";
if ($totalFixable > 0) {
echo PHP_EOL.str_repeat('-', $width).PHP_EOL;
echo "\033[1mPHPCBF CAN FIX $totalFixable OF THESE SNIFF VIOLATIONS AUTOMATICALLY\033[0m";
}
echo PHP_EOL.str_repeat('-', $width).PHP_EOL.PHP_EOL;
if ($toScreen === true && $interactive === false) {
Timing::printRunTime();
}
}//end generate()
}//end class
@@ -0,0 +1,377 @@
<?php
/**
* Version control report base class for PHP_CodeSniffer.
*
* @author Ben Selby <benmatselby@gmail.com>
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Timing;
abstract class VersionControl implements Report
{
/**
* The name of the report we want in the output.
*
* @var string
*/
protected $reportName = 'VERSION CONTROL';
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
$blames = $this->getBlameContent($phpcsFile->getFilename());
$authorCache = [];
$praiseCache = [];
$sourceCache = [];
foreach ($report['messages'] as $line => $lineErrors) {
$author = 'Unknown';
if (isset($blames[($line - 1)]) === true) {
$blameAuthor = $this->getAuthor($blames[($line - 1)]);
if ($blameAuthor !== false) {
$author = $blameAuthor;
}
}
if (isset($authorCache[$author]) === false) {
$authorCache[$author] = 0;
$praiseCache[$author] = [
'good' => 0,
'bad' => 0,
];
}
$praiseCache[$author]['bad']++;
foreach ($lineErrors as $colErrors) {
foreach ($colErrors as $error) {
$authorCache[$author]++;
if ($showSources === true) {
$source = $error['source'];
if (isset($sourceCache[$author][$source]) === false) {
$sourceCache[$author][$source] = [
'count' => 1,
'fixable' => $error['fixable'],
];
} else {
$sourceCache[$author][$source]['count']++;
}
}
}
}
unset($blames[($line - 1)]);
}//end foreach
// Now go through and give the authors some credit for
// all the lines that do not have errors.
foreach ($blames as $line) {
$author = $this->getAuthor($line);
if ($author === false) {
$author = 'Unknown';
}
if (isset($authorCache[$author]) === false) {
// This author doesn't have any errors.
if (PHP_CODESNIFFER_VERBOSITY === 0) {
continue;
}
$authorCache[$author] = 0;
$praiseCache[$author] = [
'good' => 0,
'bad' => 0,
];
}
$praiseCache[$author]['good']++;
}//end foreach
foreach ($authorCache as $author => $errors) {
echo "AUTHOR>>$author>>$errors".PHP_EOL;
}
foreach ($praiseCache as $author => $praise) {
echo "PRAISE>>$author>>".$praise['good'].'>>'.$praise['bad'].PHP_EOL;
}
foreach ($sourceCache as $author => $sources) {
foreach ($sources as $source => $sourceData) {
$count = $sourceData['count'];
$fixable = (int) $sourceData['fixable'];
echo "SOURCE>>$author>>$source>>$count>>$fixable".PHP_EOL;
}
}
return true;
}//end generateFileReport()
/**
* Prints the author of all errors and warnings, as given by "version control blame".
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
$errorsShown = ($totalErrors + $totalWarnings);
if ($errorsShown === 0) {
// Nothing to show.
return;
}
$lines = explode(PHP_EOL, $cachedData);
array_pop($lines);
if (empty($lines) === true) {
return;
}
$authorCache = [];
$praiseCache = [];
$sourceCache = [];
foreach ($lines as $line) {
$parts = explode('>>', $line);
switch ($parts[0]) {
case 'AUTHOR':
if (isset($authorCache[$parts[1]]) === false) {
$authorCache[$parts[1]] = $parts[2];
} else {
$authorCache[$parts[1]] += $parts[2];
}
break;
case 'PRAISE':
if (isset($praiseCache[$parts[1]]) === false) {
$praiseCache[$parts[1]] = [
'good' => $parts[2],
'bad' => $parts[3],
];
} else {
$praiseCache[$parts[1]]['good'] += $parts[2];
$praiseCache[$parts[1]]['bad'] += $parts[3];
}
break;
case 'SOURCE':
if (isset($praiseCache[$parts[1]]) === false) {
$praiseCache[$parts[1]] = [];
}
if (isset($sourceCache[$parts[1]][$parts[2]]) === false) {
$sourceCache[$parts[1]][$parts[2]] = [
'count' => $parts[3],
'fixable' => (bool) $parts[4],
];
} else {
$sourceCache[$parts[1]][$parts[2]]['count'] += $parts[3];
}
break;
default:
break;
}//end switch
}//end foreach
// Make sure the report width isn't too big.
$maxLength = 0;
foreach ($authorCache as $author => $count) {
$maxLength = max($maxLength, strlen($author));
if ($showSources === true && isset($sourceCache[$author]) === true) {
foreach ($sourceCache[$author] as $source => $sourceData) {
if ($source === 'count') {
continue;
}
$maxLength = max($maxLength, (strlen($source) + 9));
}
}
}
$width = min($width, ($maxLength + 30));
$width = max($width, 70);
arsort($authorCache);
echo PHP_EOL."\033[1m".'PHP CODE SNIFFER '.$this->reportName.' BLAME SUMMARY'."\033[0m".PHP_EOL;
echo str_repeat('-', $width).PHP_EOL."\033[1m";
if ($showSources === true) {
echo 'AUTHOR SOURCE'.str_repeat(' ', ($width - 43)).'(Author %) (Overall %) COUNT'.PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
} else {
echo 'AUTHOR'.str_repeat(' ', ($width - 34)).'(Author %) (Overall %) COUNT'.PHP_EOL;
echo str_repeat('-', $width).PHP_EOL;
}
echo "\033[0m";
if ($showSources === true) {
$maxSniffWidth = ($width - 15);
if ($totalFixable > 0) {
$maxSniffWidth -= 4;
}
}
$fixableSources = 0;
foreach ($authorCache as $author => $count) {
if ($praiseCache[$author]['good'] === 0) {
$percent = 0;
} else {
$total = ($praiseCache[$author]['bad'] + $praiseCache[$author]['good']);
$percent = round(($praiseCache[$author]['bad'] / $total * 100), 2);
}
$overallPercent = '('.round((($count / $errorsShown) * 100), 2).')';
$authorPercent = '('.$percent.')';
$line = str_repeat(' ', (6 - strlen($count))).$count;
$line = str_repeat(' ', (12 - strlen($overallPercent))).$overallPercent.$line;
$line = str_repeat(' ', (11 - strlen($authorPercent))).$authorPercent.$line;
$line = $author.str_repeat(' ', ($width - strlen($author) - strlen($line))).$line;
if ($showSources === true) {
$line = "\033[1m$line\033[0m";
}
echo $line.PHP_EOL;
if ($showSources === true && isset($sourceCache[$author]) === true) {
$errors = $sourceCache[$author];
asort($errors);
$errors = array_reverse($errors);
foreach ($errors as $source => $sourceData) {
if ($source === 'count') {
continue;
}
$count = $sourceData['count'];
$srcLength = strlen($source);
if ($srcLength > $maxSniffWidth) {
$source = substr($source, 0, $maxSniffWidth);
}
$line = str_repeat(' ', (5 - strlen($count))).$count;
echo ' ';
if ($totalFixable > 0) {
echo '[';
if ($sourceData['fixable'] === true) {
echo 'x';
$fixableSources++;
} else {
echo ' ';
}
echo '] ';
}
echo $source;
if ($totalFixable > 0) {
echo str_repeat(' ', ($width - 18 - strlen($source)));
} else {
echo str_repeat(' ', ($width - 14 - strlen($source)));
}
echo $line.PHP_EOL;
}//end foreach
}//end if
}//end foreach
echo str_repeat('-', $width).PHP_EOL;
echo "\033[1m".'A TOTAL OF '.$errorsShown.' SNIFF VIOLATION';
if ($errorsShown !== 1) {
echo 'S';
}
echo ' WERE COMMITTED BY '.count($authorCache).' AUTHOR';
if (count($authorCache) !== 1) {
echo 'S';
}
echo "\033[0m";
if ($totalFixable > 0) {
if ($showSources === true) {
echo PHP_EOL.str_repeat('-', $width).PHP_EOL;
echo "\033[1mPHPCBF CAN FIX THE $fixableSources MARKED SOURCES AUTOMATICALLY ($totalFixable VIOLATIONS IN TOTAL)\033[0m";
} else {
echo PHP_EOL.str_repeat('-', $width).PHP_EOL;
echo "\033[1mPHPCBF CAN FIX $totalFixable OF THESE SNIFF VIOLATIONS AUTOMATICALLY\033[0m";
}
}
echo PHP_EOL.str_repeat('-', $width).PHP_EOL.PHP_EOL;
if ($toScreen === true && $interactive === false) {
Timing::printRunTime();
}
}//end generate()
/**
* Extract the author from a blame line.
*
* @param string $line Line to parse.
*
* @return mixed string or false if impossible to recover.
*/
abstract protected function getAuthor($line);
/**
* Gets the blame output.
*
* @param string $filename File to blame.
*
* @return array
*/
abstract protected function getBlameContent($filename);
}//end class
@@ -0,0 +1,128 @@
<?php
/**
* XML report for PHP_CodeSniffer.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Reports;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use XMLWriter;
class Xml implements Report
{
/**
* Generate a partial report for a single processed file.
*
* Function should return TRUE if it printed or stored data about the file
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
* @param array<string, string|int|array> $report Prepared report data.
* See the {@see Report} interface for a detailed specification.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
*
* @return bool
*/
public function generateFileReport($report, File $phpcsFile, $showSources=false, $width=80)
{
$out = new XMLWriter;
$out->openMemory();
$out->setIndent(true);
$out->setIndentString(' ');
$out->startDocument('1.0', 'UTF-8');
if ($report['errors'] === 0 && $report['warnings'] === 0) {
// Nothing to print.
return false;
}
$out->startElement('file');
$out->writeAttribute('name', $report['filename']);
$out->writeAttribute('errors', $report['errors']);
$out->writeAttribute('warnings', $report['warnings']);
$out->writeAttribute('fixable', $report['fixable']);
foreach ($report['messages'] as $line => $lineErrors) {
foreach ($lineErrors as $column => $colErrors) {
foreach ($colErrors as $error) {
$error['type'] = strtolower($error['type']);
if ($phpcsFile->config->encoding !== 'utf-8') {
$error['message'] = iconv($phpcsFile->config->encoding, 'utf-8', $error['message']);
}
$out->startElement($error['type']);
$out->writeAttribute('line', $line);
$out->writeAttribute('column', $column);
$out->writeAttribute('source', $error['source']);
$out->writeAttribute('severity', $error['severity']);
$out->writeAttribute('fixable', (int) $error['fixable']);
$out->text($error['message']);
$out->endElement();
}
}
}//end foreach
$out->endElement();
// Remove the start of the document because we will
// add that manually later. We only have it in here to
// properly set the encoding.
$content = $out->flush();
if (strpos($content, PHP_EOL) !== false) {
$content = substr($content, (strpos($content, PHP_EOL) + strlen(PHP_EOL)));
} else if (strpos($content, "\n") !== false) {
$content = substr($content, (strpos($content, "\n") + 1));
}
echo $content;
return true;
}//end generateFileReport()
/**
* Prints all violations for processed files, in a proprietary XML format.
*
* @param string $cachedData Any partial report data that was returned from
* generateFileReport during the run.
* @param int $totalFiles Total number of files processed during the run.
* @param int $totalErrors Total number of errors found during the run.
* @param int $totalWarnings Total number of warnings found during the run.
* @param int $totalFixable Total number of problems that can be fixed.
* @param bool $showSources Show sources?
* @param int $width Maximum allowed line width.
* @param bool $interactive Are we running in interactive mode?
* @param bool $toScreen Is the report being printed to screen?
*
* @return void
*/
public function generate(
$cachedData,
$totalFiles,
$totalErrors,
$totalWarnings,
$totalFixable,
$showSources=false,
$width=80,
$interactive=false,
$toScreen=true
) {
echo '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
echo '<phpcs version="'.Config::VERSION.'">'.PHP_EOL;
echo $cachedData;
echo '</phpcs>'.PHP_EOL;
}//end generate()
}//end class
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,992 @@
<?php
/**
* Responsible for running PHPCS and PHPCBF.
*
* After creating an object of this class, you probably just want to
* call runPHPCS() or runPHPCBF().
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer;
use Exception;
use InvalidArgumentException;
use PHP_CodeSniffer\Exceptions\DeepExitException;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\DummyFile;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Files\FileList;
use PHP_CodeSniffer\Util\Cache;
use PHP_CodeSniffer\Util\Common;
use PHP_CodeSniffer\Util\Standards;
use PHP_CodeSniffer\Util\Timing;
use PHP_CodeSniffer\Util\Tokens;
class Runner
{
/**
* The config data for the run.
*
* @var \PHP_CodeSniffer\Config
*/
public $config = null;
/**
* The ruleset used for the run.
*
* @var \PHP_CodeSniffer\Ruleset
*/
public $ruleset = null;
/**
* The reporter used for generating reports after the run.
*
* @var \PHP_CodeSniffer\Reporter
*/
public $reporter = null;
/**
* Run the PHPCS script.
*
* @return int
*/
public function runPHPCS()
{
$this->registerOutOfMemoryShutdownMessage('phpcs');
try {
Timing::startTiming();
Runner::checkRequirements();
if (defined('PHP_CODESNIFFER_CBF') === false) {
define('PHP_CODESNIFFER_CBF', false);
}
// Creating the Config object populates it with all required settings
// based on the CLI arguments provided to the script and any config
// values the user has set.
$this->config = new Config();
// Init the run and load the rulesets to set additional config vars.
$this->init();
// Print a list of sniffs in each of the supplied standards.
// We fudge the config here so that each standard is explained in isolation.
if ($this->config->explain === true) {
$standards = $this->config->standards;
foreach ($standards as $standard) {
$this->config->standards = [$standard];
$ruleset = new Ruleset($this->config);
$ruleset->explain();
}
return 0;
}
// Generate documentation for each of the supplied standards.
if ($this->config->generator !== null) {
$standards = $this->config->standards;
foreach ($standards as $standard) {
$this->config->standards = [$standard];
$ruleset = new Ruleset($this->config);
$class = 'PHP_CodeSniffer\Generators\\'.$this->config->generator;
$generator = new $class($ruleset);
$generator->generate();
}
return 0;
}
// Other report formats don't really make sense in interactive mode
// so we hard-code the full report here and when outputting.
// We also ensure parallel processing is off because we need to do one file at a time.
if ($this->config->interactive === true) {
$this->config->reports = ['full' => null];
$this->config->parallel = 1;
$this->config->showProgress = false;
}
// Disable caching if we are processing STDIN as we can't be 100%
// sure where the file came from or if it will change in the future.
if ($this->config->stdin === true) {
$this->config->cache = false;
}
$numErrors = $this->run();
// Print all the reports for this run.
$toScreen = $this->reporter->printReports();
// Only print timer output if no reports were
// printed to the screen so we don't put additional output
// in something like an XML report. If we are printing to screen,
// the report types would have already worked out who should
// print the timer info.
if ($this->config->interactive === false
&& ($toScreen === false
|| (($this->reporter->totalErrors + $this->reporter->totalWarnings) === 0 && $this->config->showProgress === true))
) {
Timing::printRunTime();
}
} catch (DeepExitException $e) {
echo $e->getMessage();
return $e->getCode();
}//end try
if ($numErrors === 0) {
// No errors found.
return 0;
} else if ($this->reporter->totalFixable === 0) {
// Errors found, but none of them can be fixed by PHPCBF.
return 1;
} else {
// Errors found, and some can be fixed by PHPCBF.
return 2;
}
}//end runPHPCS()
/**
* Run the PHPCBF script.
*
* @return int
*/
public function runPHPCBF()
{
$this->registerOutOfMemoryShutdownMessage('phpcbf');
if (defined('PHP_CODESNIFFER_CBF') === false) {
define('PHP_CODESNIFFER_CBF', true);
}
try {
Timing::startTiming();
Runner::checkRequirements();
// Creating the Config object populates it with all required settings
// based on the CLI arguments provided to the script and any config
// values the user has set.
$this->config = new Config();
// When processing STDIN, we can't output anything to the screen
// or it will end up mixed in with the file output.
if ($this->config->stdin === true) {
$this->config->verbosity = 0;
}
// Init the run and load the rulesets to set additional config vars.
$this->init();
// When processing STDIN, we only process one file at a time and
// we don't process all the way through, so we can't use the parallel
// running system.
if ($this->config->stdin === true) {
$this->config->parallel = 1;
}
// Override some of the command line settings that might break the fixes.
$this->config->generator = null;
$this->config->explain = false;
$this->config->interactive = false;
$this->config->cache = false;
$this->config->showSources = false;
$this->config->recordErrors = false;
$this->config->reportFile = null;
// Only use the "Cbf" report, but allow for the Performance report as well.
$originalReports = array_change_key_case($this->config->reports, CASE_LOWER);
$newReports = ['cbf' => null];
if (array_key_exists('performance', $originalReports) === true) {
$newReports['performance'] = $originalReports['performance'];
}
$this->config->reports = $newReports;
// If a standard tries to set command line arguments itself, some
// may be blocked because PHPCBF is running, so stop the script
// dying if any are found.
$this->config->dieOnUnknownArg = false;
$this->run();
$this->reporter->printReports();
echo PHP_EOL;
Timing::printRunTime();
} catch (DeepExitException $e) {
echo $e->getMessage();
return $e->getCode();
}//end try
if ($this->reporter->totalFixed === 0) {
// Nothing was fixed by PHPCBF.
if ($this->reporter->totalFixable === 0) {
// Nothing found that could be fixed.
return 0;
} else {
// Something failed to fix.
return 2;
}
}
if ($this->reporter->totalFixable === 0) {
// PHPCBF fixed all fixable errors.
return 1;
}
// PHPCBF fixed some fixable errors, but others failed to fix.
return 2;
}//end runPHPCBF()
/**
* Exits if the minimum requirements of PHP_CodeSniffer are not met.
*
* @return void
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException If the requirements are not met.
*/
public function checkRequirements()
{
// Check the PHP version.
if (PHP_VERSION_ID < 50400) {
$error = 'ERROR: PHP_CodeSniffer requires PHP version 5.4.0 or greater.'.PHP_EOL;
throw new DeepExitException($error, 3);
}
$requiredExtensions = [
'tokenizer',
'xmlwriter',
'SimpleXML',
];
$missingExtensions = [];
foreach ($requiredExtensions as $extension) {
if (extension_loaded($extension) === false) {
$missingExtensions[] = $extension;
}
}
if (empty($missingExtensions) === false) {
$last = array_pop($requiredExtensions);
$required = implode(', ', $requiredExtensions);
$required .= ' and '.$last;
if (count($missingExtensions) === 1) {
$missing = $missingExtensions[0];
} else {
$last = array_pop($missingExtensions);
$missing = implode(', ', $missingExtensions);
$missing .= ' and '.$last;
}
$error = 'ERROR: PHP_CodeSniffer requires the %s extensions to be enabled. Please enable %s.'.PHP_EOL;
$error = sprintf($error, $required, $missing);
throw new DeepExitException($error, 3);
}
}//end checkRequirements()
/**
* Init the rulesets and other high-level settings.
*
* @return void
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException If a referenced standard is not installed.
*/
public function init()
{
if (defined('PHP_CODESNIFFER_CBF') === false) {
define('PHP_CODESNIFFER_CBF', false);
}
// Ensure this option is enabled or else line endings will not always
// be detected properly for files created on a Mac with the /r line ending.
@ini_set('auto_detect_line_endings', true);
// Disable the PCRE JIT as this caused issues with parallel running.
ini_set('pcre.jit', false);
// Check that the standards are valid.
foreach ($this->config->standards as $standard) {
if (Standards::isInstalledStandard($standard) === false) {
// They didn't select a valid coding standard, so help them
// out by letting them know which standards are installed.
$error = 'ERROR: the "'.$standard.'" coding standard is not installed. ';
ob_start();
Standards::printInstalledStandards();
$error .= ob_get_contents();
ob_end_clean();
throw new DeepExitException($error, 3);
}
}
// Saves passing the Config object into other objects that only need
// the verbosity flag for debug output.
if (defined('PHP_CODESNIFFER_VERBOSITY') === false) {
define('PHP_CODESNIFFER_VERBOSITY', $this->config->verbosity);
}
// Create this class so it is autoloaded and sets up a bunch
// of PHP_CodeSniffer-specific token type constants.
new Tokens();
// Allow autoloading of custom files inside installed standards.
$installedStandards = Standards::getInstalledStandardDetails();
foreach ($installedStandards as $details) {
Autoload::addSearchPath($details['path'], $details['namespace']);
}
// The ruleset contains all the information about how the files
// should be checked and/or fixed.
try {
$this->ruleset = new Ruleset($this->config);
if ($this->ruleset->hasSniffDeprecations() === true) {
$this->ruleset->showSniffDeprecations();
}
} catch (RuntimeException $e) {
$error = 'ERROR: '.$e->getMessage().PHP_EOL.PHP_EOL;
$error .= $this->config->printShortUsage(true);
throw new DeepExitException($error, 3);
}
}//end init()
/**
* Performs the run.
*
* @return int The number of errors and warnings found.
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException
*/
private function run()
{
// The class that manages all reporters for the run.
$this->reporter = new Reporter($this->config);
// Include bootstrap files.
foreach ($this->config->bootstrap as $bootstrap) {
include $bootstrap;
}
if ($this->config->stdin === true) {
$fileContents = $this->config->stdinContent;
if ($fileContents === null) {
$handle = fopen('php://stdin', 'r');
stream_set_blocking($handle, true);
$fileContents = stream_get_contents($handle);
fclose($handle);
}
$todo = new FileList($this->config, $this->ruleset);
$dummy = new DummyFile($fileContents, $this->ruleset, $this->config);
$todo->addFile($dummy->path, $dummy);
} else {
if (empty($this->config->files) === true) {
$error = 'ERROR: You must supply at least one file or directory to process.'.PHP_EOL.PHP_EOL;
$error .= $this->config->printShortUsage(true);
throw new DeepExitException($error, 3);
}
if (PHP_CODESNIFFER_VERBOSITY > 0) {
echo 'Creating file list... ';
}
$todo = new FileList($this->config, $this->ruleset);
if (PHP_CODESNIFFER_VERBOSITY > 0) {
$numFiles = count($todo);
echo "DONE ($numFiles files in queue)".PHP_EOL;
}
if ($this->config->cache === true) {
if (PHP_CODESNIFFER_VERBOSITY > 0) {
echo 'Loading cache... ';
}
Cache::load($this->ruleset, $this->config);
if (PHP_CODESNIFFER_VERBOSITY > 0) {
$size = Cache::getSize();
echo "DONE ($size files in cache)".PHP_EOL;
}
}
}//end if
// Turn all sniff errors into exceptions.
set_error_handler([$this, 'handleErrors']);
// If verbosity is too high, turn off parallelism so the
// debug output is clean.
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$this->config->parallel = 1;
}
// If the PCNTL extension isn't installed, we can't fork.
if (function_exists('pcntl_fork') === false) {
$this->config->parallel = 1;
}
$lastDir = '';
$numFiles = count($todo);
if ($this->config->parallel === 1) {
// Running normally.
$numProcessed = 0;
foreach ($todo as $path => $file) {
if ($file->ignored === false) {
$currDir = dirname($path);
if ($lastDir !== $currDir) {
if (PHP_CODESNIFFER_VERBOSITY > 0) {
echo 'Changing into directory '.Common::stripBasepath($currDir, $this->config->basepath).PHP_EOL;
}
$lastDir = $currDir;
}
$this->processFile($file);
} else if (PHP_CODESNIFFER_VERBOSITY > 0) {
echo 'Skipping '.basename($file->path).PHP_EOL;
}
$numProcessed++;
$this->printProgress($file, $numFiles, $numProcessed);
}
} else {
// Batching and forking.
$childProcs = [];
$numPerBatch = ceil($numFiles / $this->config->parallel);
for ($batch = 0; $batch < $this->config->parallel; $batch++) {
$startAt = ($batch * $numPerBatch);
if ($startAt >= $numFiles) {
break;
}
$endAt = ($startAt + $numPerBatch);
if ($endAt > $numFiles) {
$endAt = $numFiles;
}
$childOutFilename = tempnam(sys_get_temp_dir(), 'phpcs-child');
$pid = pcntl_fork();
if ($pid === -1) {
throw new RuntimeException('Failed to create child process');
} else if ($pid !== 0) {
$childProcs[$pid] = $childOutFilename;
} else {
// Move forward to the start of the batch.
$todo->rewind();
for ($i = 0; $i < $startAt; $i++) {
$todo->next();
}
// Reset the reporter to make sure only figures from this
// file batch are recorded.
$this->reporter->totalFiles = 0;
$this->reporter->totalErrors = 0;
$this->reporter->totalWarnings = 0;
$this->reporter->totalFixable = 0;
$this->reporter->totalFixed = 0;
// Process the files.
$pathsProcessed = [];
ob_start();
for ($i = $startAt; $i < $endAt; $i++) {
$path = $todo->key();
$file = $todo->current();
if ($file->ignored === true) {
$todo->next();
continue;
}
$currDir = dirname($path);
if ($lastDir !== $currDir) {
if (PHP_CODESNIFFER_VERBOSITY > 0) {
echo 'Changing into directory '.Common::stripBasepath($currDir, $this->config->basepath).PHP_EOL;
}
$lastDir = $currDir;
}
$this->processFile($file);
$pathsProcessed[] = $path;
$todo->next();
}//end for
$debugOutput = ob_get_contents();
ob_end_clean();
// Write information about the run to the filesystem
// so it can be picked up by the main process.
$childOutput = [
'totalFiles' => $this->reporter->totalFiles,
'totalErrors' => $this->reporter->totalErrors,
'totalWarnings' => $this->reporter->totalWarnings,
'totalFixable' => $this->reporter->totalFixable,
'totalFixed' => $this->reporter->totalFixed,
];
$output = '<'.'?php'."\n".' $childOutput = ';
$output .= var_export($childOutput, true);
$output .= ";\n\$debugOutput = ";
$output .= var_export($debugOutput, true);
if ($this->config->cache === true) {
$childCache = [];
foreach ($pathsProcessed as $path) {
$childCache[$path] = Cache::get($path);
}
$output .= ";\n\$childCache = ";
$output .= var_export($childCache, true);
}
$output .= ";\n?".'>';
file_put_contents($childOutFilename, $output);
exit();
}//end if
}//end for
$success = $this->processChildProcs($childProcs);
if ($success === false) {
throw new RuntimeException('One or more child processes failed to run');
}
}//end if
restore_error_handler();
if (PHP_CODESNIFFER_VERBOSITY === 0
&& $this->config->interactive === false
&& $this->config->showProgress === true
) {
echo PHP_EOL.PHP_EOL;
}
if ($this->config->cache === true) {
Cache::save();
}
$ignoreWarnings = Config::getConfigData('ignore_warnings_on_exit');
$ignoreErrors = Config::getConfigData('ignore_errors_on_exit');
$return = ($this->reporter->totalErrors + $this->reporter->totalWarnings);
if ($ignoreErrors !== null) {
$ignoreErrors = (bool) $ignoreErrors;
if ($ignoreErrors === true) {
$return -= $this->reporter->totalErrors;
}
}
if ($ignoreWarnings !== null) {
$ignoreWarnings = (bool) $ignoreWarnings;
if ($ignoreWarnings === true) {
$return -= $this->reporter->totalWarnings;
}
}
return $return;
}//end run()
/**
* Converts all PHP errors into exceptions.
*
* This method forces a sniff to stop processing if it is not
* able to handle a specific piece of code, instead of continuing
* and potentially getting into a loop.
*
* @param int $code The level of error raised.
* @param string $message The error message.
* @param string $file The path of the file that raised the error.
* @param int $line The line number the error was raised at.
*
* @return bool
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException
*/
public function handleErrors($code, $message, $file, $line)
{
if ((error_reporting() & $code) === 0) {
// This type of error is being muted.
return true;
}
throw new RuntimeException("$message in $file on line $line");
}//end handleErrors()
/**
* Processes a single file, including checking and fixing.
*
* @param \PHP_CodeSniffer\Files\File $file The file to be processed.
*
* @return void
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException
*/
public function processFile($file)
{
if (PHP_CODESNIFFER_VERBOSITY > 0) {
$startTime = microtime(true);
echo 'Processing '.basename($file->path).' ';
if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo PHP_EOL;
}
}
try {
$file->process();
if (PHP_CODESNIFFER_VERBOSITY > 0) {
$timeTaken = ((microtime(true) - $startTime) * 1000);
if ($timeTaken < 1000) {
$timeTaken = round($timeTaken);
echo "DONE in {$timeTaken}ms";
} else {
$timeTaken = round(($timeTaken / 1000), 2);
echo "DONE in $timeTaken secs";
}
if (PHP_CODESNIFFER_CBF === true) {
$errors = $file->getFixableCount();
echo " ($errors fixable violations)".PHP_EOL;
} else {
$errors = $file->getErrorCount();
$warnings = $file->getWarningCount();
echo " ($errors errors, $warnings warnings)".PHP_EOL;
}
}
} catch (Exception $e) {
$error = 'An error occurred during processing; checking has been aborted. The error message was: '.$e->getMessage();
// Determine which sniff caused the error.
$sniffStack = null;
$nextStack = null;
foreach ($e->getTrace() as $step) {
if (isset($step['file']) === false) {
continue;
}
if (empty($sniffStack) === false) {
$nextStack = $step;
break;
}
if (substr($step['file'], -9) === 'Sniff.php') {
$sniffStack = $step;
continue;
}
}
if (empty($sniffStack) === false) {
$sniffCode = '';
try {
if (empty($nextStack) === false
&& isset($nextStack['class']) === true
&& substr($nextStack['class'], -5) === 'Sniff'
) {
$sniffCode = 'the '.Common::getSniffCode($nextStack['class']).' sniff';
}
} catch (InvalidArgumentException $e) {
// Sniff code could not be determined. This may be an abstract sniff class.
}
if ($sniffCode === '') {
$sniffCode = substr(strrchr(str_replace('\\', '/', $sniffStack['file']), '/'), 1);
}
$error .= sprintf(PHP_EOL.'The error originated in %s on line %s.', $sniffCode, $sniffStack['line']);
}
$file->addErrorOnLine($error, 1, 'Internal.Exception');
}//end try
$this->reporter->cacheFileReport($file);
if ($this->config->interactive === true) {
/*
Running interactively.
Print the error report for the current file and then wait for user input.
*/
// Get current violations and then clear the list to make sure
// we only print violations for a single file each time.
$numErrors = null;
while ($numErrors !== 0) {
$numErrors = ($file->getErrorCount() + $file->getWarningCount());
if ($numErrors === 0) {
continue;
}
$this->reporter->printReport('full');
echo '<ENTER> to recheck, [s] to skip or [q] to quit : ';
$input = fgets(STDIN);
$input = trim($input);
switch ($input) {
case 's':
break(2);
case 'q':
throw new DeepExitException('', 0);
default:
// Repopulate the sniffs because some of them save their state
// and only clear it when the file changes, but we are rechecking
// the same file.
$file->ruleset->populateTokenListeners();
$file->reloadContent();
$file->process();
$this->reporter->cacheFileReport($file);
break;
}
}//end while
}//end if
// Clean up the file to save (a lot of) memory.
$file->cleanUp();
}//end processFile()
/**
* Waits for child processes to complete and cleans up after them.
*
* The reporting information returned by each child process is merged
* into the main reporter class.
*
* @param array $childProcs An array of child processes to wait for.
*
* @return bool
*/
private function processChildProcs($childProcs)
{
$numProcessed = 0;
$totalBatches = count($childProcs);
$success = true;
while (count($childProcs) > 0) {
$pid = pcntl_waitpid(0, $status);
if ($pid <= 0) {
continue;
}
$childProcessStatus = pcntl_wexitstatus($status);
if ($childProcessStatus !== 0) {
$success = false;
}
$out = $childProcs[$pid];
unset($childProcs[$pid]);
if (file_exists($out) === false) {
continue;
}
include $out;
unlink($out);
$numProcessed++;
if (isset($childOutput) === false) {
// The child process died, so the run has failed.
$file = new DummyFile('', $this->ruleset, $this->config);
$file->setErrorCounts(1, 0, 0, 0);
$this->printProgress($file, $totalBatches, $numProcessed);
$success = false;
continue;
}
$this->reporter->totalFiles += $childOutput['totalFiles'];
$this->reporter->totalErrors += $childOutput['totalErrors'];
$this->reporter->totalWarnings += $childOutput['totalWarnings'];
$this->reporter->totalFixable += $childOutput['totalFixable'];
$this->reporter->totalFixed += $childOutput['totalFixed'];
if (isset($debugOutput) === true) {
echo $debugOutput;
}
if (isset($childCache) === true) {
foreach ($childCache as $path => $cache) {
Cache::set($path, $cache);
}
}
// Fake a processed file so we can print progress output for the batch.
$file = new DummyFile('', $this->ruleset, $this->config);
$file->setErrorCounts(
$childOutput['totalErrors'],
$childOutput['totalWarnings'],
$childOutput['totalFixable'],
$childOutput['totalFixed']
);
$this->printProgress($file, $totalBatches, $numProcessed);
}//end while
return $success;
}//end processChildProcs()
/**
* Print progress information for a single processed file.
*
* @param \PHP_CodeSniffer\Files\File $file The file that was processed.
* @param int $numFiles The total number of files to process.
* @param int $numProcessed The number of files that have been processed,
* including this one.
*
* @return void
*/
public function printProgress(File $file, $numFiles, $numProcessed)
{
if (PHP_CODESNIFFER_VERBOSITY > 0
|| $this->config->showProgress === false
) {
return;
}
// Show progress information.
if ($file->ignored === true) {
echo 'S';
} else {
$errors = $file->getErrorCount();
$warnings = $file->getWarningCount();
$fixable = $file->getFixableCount();
$fixed = $file->getFixedCount();
if (PHP_CODESNIFFER_CBF === true) {
// Files with fixed errors or warnings are F (green).
// Files with unfixable errors or warnings are E (red).
// Files with no errors or warnings are . (black).
if ($fixable > 0) {
if ($this->config->colors === true) {
echo "\033[31m";
}
echo 'E';
if ($this->config->colors === true) {
echo "\033[0m";
}
} else if ($fixed > 0) {
if ($this->config->colors === true) {
echo "\033[32m";
}
echo 'F';
if ($this->config->colors === true) {
echo "\033[0m";
}
} else {
echo '.';
}//end if
} else {
// Files with errors are E (red).
// Files with fixable errors are E (green).
// Files with warnings are W (yellow).
// Files with fixable warnings are W (green).
// Files with no errors or warnings are . (black).
if ($errors > 0) {
if ($this->config->colors === true) {
if ($fixable > 0) {
echo "\033[32m";
} else {
echo "\033[31m";
}
}
echo 'E';
if ($this->config->colors === true) {
echo "\033[0m";
}
} else if ($warnings > 0) {
if ($this->config->colors === true) {
if ($fixable > 0) {
echo "\033[32m";
} else {
echo "\033[33m";
}
}
echo 'W';
if ($this->config->colors === true) {
echo "\033[0m";
}
} else {
echo '.';
}//end if
}//end if
}//end if
$numPerLine = 60;
if ($numProcessed !== $numFiles && ($numProcessed % $numPerLine) !== 0) {
return;
}
$percent = round(($numProcessed / $numFiles) * 100);
$padding = (strlen($numFiles) - strlen($numProcessed));
if ($numProcessed === $numFiles
&& $numFiles > $numPerLine
&& ($numProcessed % $numPerLine) !== 0
) {
$padding += ($numPerLine - ($numFiles - (floor($numFiles / $numPerLine) * $numPerLine)));
}
echo str_repeat(' ', $padding)." $numProcessed / $numFiles ($percent%)".PHP_EOL;
}//end printProgress()
/**
* Registers a PHP shutdown function to provide a more informative out of memory error.
*
* @param string $command The command which was used to initiate the PHPCS run.
*
* @return void
*/
private function registerOutOfMemoryShutdownMessage($command)
{
// Allocate all needed memory beforehand as much as possible.
$errorMsg = PHP_EOL.'The PHP_CodeSniffer "%1$s" command ran out of memory.'.PHP_EOL;
$errorMsg .= 'Either raise the "memory_limit" of PHP in the php.ini file or raise the memory limit at runtime'.PHP_EOL;
$errorMsg .= 'using `%1$s -d memory_limit=512M` (replace 512M with the desired memory limit).'.PHP_EOL;
$errorMsg = sprintf($errorMsg, $command);
$memoryError = 'Allowed memory size of';
$errorArray = [
'type' => 42,
'message' => 'Some random dummy string to take up memory and take up some more memory and some more',
'file' => 'Another random string, which would be a filename this time. Should be relatively long to allow for deeply nested files',
'line' => 31427,
];
register_shutdown_function(
static function () use (
$errorMsg,
$memoryError,
$errorArray
) {
$errorArray = error_get_last();
if (is_array($errorArray) === true && strpos($errorArray['message'], $memoryError) !== false) {
echo $errorMsg;
}
}
);
}//end registerOutOfMemoryShutdownMessage()
}//end class
@@ -0,0 +1,941 @@
<?php
/**
* Processes pattern strings and checks that the code conforms to the pattern.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Sniffs;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Tokenizers\PHP;
use PHP_CodeSniffer\Util\Tokens;
abstract class AbstractPatternSniff implements Sniff
{
/**
* If true, comments will be ignored if they are found in the code.
*
* @var boolean
*/
public $ignoreComments = false;
/**
* The current file being checked.
*
* @var string
*/
protected $currFile = '';
/**
* The parsed patterns array.
*
* @var array
*/
private $parsedPatterns = [];
/**
* Tokens that this sniff wishes to process outside of the patterns.
*
* @var int[]
* @see registerSupplementary()
* @see processSupplementary()
*/
private $supplementaryTokens = [];
/**
* Positions in the stack where errors have occurred.
*
* @var array<int, bool>
*/
private $errorPos = [];
/**
* Constructs a AbstractPatternSniff.
*
* @param boolean $ignoreComments If true, comments will be ignored.
*/
public function __construct($ignoreComments=null)
{
// This is here for backwards compatibility.
if ($ignoreComments !== null) {
$this->ignoreComments = $ignoreComments;
}
$this->supplementaryTokens = $this->registerSupplementary();
}//end __construct()
/**
* Registers the tokens to listen to.
*
* Classes extending <i>AbstractPatternTest</i> should implement the
* <i>getPatterns()</i> method to register the patterns they wish to test.
*
* @return array<int|string>
* @see process()
*/
final public function register()
{
$listenTypes = [];
$patterns = $this->getPatterns();
foreach ($patterns as $pattern) {
$parsedPattern = $this->parse($pattern);
// Find a token position in the pattern that we can use
// for a listener token.
$pos = $this->getListenerTokenPos($parsedPattern);
$tokenType = $parsedPattern[$pos]['token'];
$listenTypes[] = $tokenType;
$patternArray = [
'listen_pos' => $pos,
'pattern' => $parsedPattern,
'pattern_code' => $pattern,
];
if (isset($this->parsedPatterns[$tokenType]) === false) {
$this->parsedPatterns[$tokenType] = [];
}
$this->parsedPatterns[$tokenType][] = $patternArray;
}//end foreach
return array_unique(array_merge($listenTypes, $this->supplementaryTokens));
}//end register()
/**
* Returns the token types that the specified pattern is checking for.
*
* Returned array is in the format:
* <code>
* array(
* T_WHITESPACE => 0, // 0 is the position where the T_WHITESPACE token
* // should occur in the pattern.
* );
* </code>
*
* @param array $pattern The parsed pattern to find the acquire the token
* types from.
*
* @return array<int, int>
*/
private function getPatternTokenTypes($pattern)
{
$tokenTypes = [];
foreach ($pattern as $pos => $patternInfo) {
if ($patternInfo['type'] === 'token') {
if (isset($tokenTypes[$patternInfo['token']]) === false) {
$tokenTypes[$patternInfo['token']] = $pos;
}
}
}
return $tokenTypes;
}//end getPatternTokenTypes()
/**
* Returns the position in the pattern that this test should register as
* a listener for the pattern.
*
* @param array $pattern The pattern to acquire the listener for.
*
* @return int The position in the pattern that this test should register
* as the listener.
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If we could not determine a token to listen for.
*/
private function getListenerTokenPos($pattern)
{
$tokenTypes = $this->getPatternTokenTypes($pattern);
$tokenCodes = array_keys($tokenTypes);
$token = Tokens::getHighestWeightedToken($tokenCodes);
// If we could not get a token.
if ($token === false) {
$error = 'Could not determine a token to listen for';
throw new RuntimeException($error);
}
return $tokenTypes[$token];
}//end getListenerTokenPos()
/**
* Processes the test.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token occurred.
* @param int $stackPtr The position in the tokens stack
* where the listening token type
* was found.
*
* @return void
* @see register()
*/
final public function process(File $phpcsFile, $stackPtr)
{
$file = $phpcsFile->getFilename();
if ($this->currFile !== $file) {
// We have changed files, so clean up.
$this->errorPos = [];
$this->currFile = $file;
}
$tokens = $phpcsFile->getTokens();
if (in_array($tokens[$stackPtr]['code'], $this->supplementaryTokens, true) === true) {
$this->processSupplementary($phpcsFile, $stackPtr);
}
$type = $tokens[$stackPtr]['code'];
// If the type is not set, then it must have been a token registered
// with registerSupplementary().
if (isset($this->parsedPatterns[$type]) === false) {
return;
}
$allErrors = [];
// Loop over each pattern that is listening to the current token type
// that we are processing.
foreach ($this->parsedPatterns[$type] as $patternInfo) {
// If processPattern returns false, then the pattern that we are
// checking the code with must not be designed to check that code.
$errors = $this->processPattern($patternInfo, $phpcsFile, $stackPtr);
if ($errors === false) {
// The pattern didn't match.
continue;
} else if (empty($errors) === true) {
// The pattern matched, but there were no errors.
break;
}
foreach ($errors as $stackPtr => $error) {
if (isset($this->errorPos[$stackPtr]) === false) {
$this->errorPos[$stackPtr] = true;
$allErrors[$stackPtr] = $error;
}
}
}
foreach ($allErrors as $stackPtr => $error) {
$phpcsFile->addError($error, $stackPtr, 'Found');
}
}//end process()
/**
* Processes the pattern and verifies the code at $stackPtr.
*
* @param array $patternInfo Information about the pattern used
* for checking, which includes are
* parsed token representation of the
* pattern.
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token occurred.
* @param int $stackPtr The position in the tokens stack where
* the listening token type was found.
*
* @return array|false
*/
protected function processPattern($patternInfo, File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$pattern = $patternInfo['pattern'];
$patternCode = $patternInfo['pattern_code'];
$errors = [];
$found = '';
$ignoreTokens = [T_WHITESPACE => T_WHITESPACE];
if ($this->ignoreComments === true) {
$ignoreTokens += Tokens::$commentTokens;
}
$origStackPtr = $stackPtr;
$hasError = false;
if ($patternInfo['listen_pos'] > 0) {
$stackPtr--;
for ($i = ($patternInfo['listen_pos'] - 1); $i >= 0; $i--) {
if ($pattern[$i]['type'] === 'token') {
if ($pattern[$i]['token'] === T_WHITESPACE) {
if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
$found = $tokens[$stackPtr]['content'].$found;
}
// Only check the size of the whitespace if this is not
// the first token. We don't care about the size of
// leading whitespace, just that there is some.
if ($i !== 0) {
if ($tokens[$stackPtr]['content'] !== $pattern[$i]['value']) {
$hasError = true;
}
}
} else {
// Check to see if this important token is the same as the
// previous important token in the pattern. If it is not,
// then the pattern cannot be for this piece of code.
$prev = $phpcsFile->findPrevious(
$ignoreTokens,
$stackPtr,
null,
true
);
if ($prev === false
|| $tokens[$prev]['code'] !== $pattern[$i]['token']
) {
return false;
}
// If we skipped past some whitespace tokens, then add them
// to the found string.
$tokenContent = $phpcsFile->getTokensAsString(
($prev + 1),
($stackPtr - $prev - 1)
);
$found = $tokens[$prev]['content'].$tokenContent.$found;
if (isset($pattern[($i - 1)]) === true
&& $pattern[($i - 1)]['type'] === 'skip'
) {
$stackPtr = $prev;
} else {
$stackPtr = ($prev - 1);
}
}//end if
} else if ($pattern[$i]['type'] === 'skip') {
// Skip to next piece of relevant code.
if ($pattern[$i]['to'] === 'parenthesis_closer') {
$to = 'parenthesis_opener';
} else {
$to = 'scope_opener';
}
// Find the previous opener.
$next = $phpcsFile->findPrevious(
$ignoreTokens,
$stackPtr,
null,
true
);
if ($next === false || isset($tokens[$next][$to]) === false) {
// If there was not opener, then we must be
// using the wrong pattern.
return false;
}
if ($to === 'parenthesis_opener') {
$found = '{'.$found;
} else {
$found = '('.$found;
}
$found = '...'.$found;
// Skip to the opening token.
$stackPtr = ($tokens[$next][$to] - 1);
} else if ($pattern[$i]['type'] === 'string') {
$found = 'abc';
} else if ($pattern[$i]['type'] === 'newline') {
if ($this->ignoreComments === true
&& isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true
) {
$startComment = $phpcsFile->findPrevious(
Tokens::$commentTokens,
($stackPtr - 1),
null,
true
);
if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1)]['line']) {
$startComment++;
}
$tokenContent = $phpcsFile->getTokensAsString(
$startComment,
($stackPtr - $startComment + 1)
);
$found = $tokenContent.$found;
$stackPtr = ($startComment - 1);
}
if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar) {
$found = $tokens[$stackPtr]['content'].$found;
// This may just be an indent that comes after a newline
// so check the token before to make sure. If it is a newline, we
// can ignore the error here.
if (($tokens[($stackPtr - 1)]['content'] !== $phpcsFile->eolChar)
&& ($this->ignoreComments === true
&& isset(Tokens::$commentTokens[$tokens[($stackPtr - 1)]['code']]) === false)
) {
$hasError = true;
} else {
$stackPtr--;
}
} else {
$found = 'EOL'.$found;
}
} else {
$found = $tokens[$stackPtr]['content'].$found;
$hasError = true;
}//end if
if ($hasError === false && $pattern[($i - 1)]['type'] !== 'newline') {
// Make sure they only have 1 newline.
$prev = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true);
if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) {
$hasError = true;
}
}
}//end if
}//end for
}//end if
$stackPtr = $origStackPtr;
$lastAddedStackPtr = null;
$patternLen = count($pattern);
if (($stackPtr + $patternLen - $patternInfo['listen_pos']) > $phpcsFile->numTokens) {
// Pattern can never match as there are not enough tokens left in the file.
return false;
}
for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) {
if (isset($tokens[$stackPtr]) === false) {
break;
}
if ($pattern[$i]['type'] === 'token') {
if ($pattern[$i]['token'] === T_WHITESPACE) {
if ($this->ignoreComments === true) {
// If we are ignoring comments, check to see if this current
// token is a comment. If so skip it.
if (isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true) {
continue;
}
// If the next token is a comment, the we need to skip the
// current token as we should allow a space before a
// comment for readability.
if (isset($tokens[($stackPtr + 1)]) === true
&& isset(Tokens::$commentTokens[$tokens[($stackPtr + 1)]['code']]) === true
) {
continue;
}
}
$tokenContent = '';
if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
if (isset($pattern[($i + 1)]) === false) {
// This is the last token in the pattern, so just compare
// the next token of content.
$tokenContent = $tokens[$stackPtr]['content'];
} else {
// Get all the whitespace to the next token.
$next = $phpcsFile->findNext(
Tokens::$emptyTokens,
$stackPtr,
null,
true
);
$tokenContent = $phpcsFile->getTokensAsString(
$stackPtr,
($next - $stackPtr)
);
$lastAddedStackPtr = $stackPtr;
$stackPtr = $next;
}//end if
if ($stackPtr !== $lastAddedStackPtr) {
$found .= $tokenContent;
}
} else {
if ($stackPtr !== $lastAddedStackPtr) {
$found .= $tokens[$stackPtr]['content'];
$lastAddedStackPtr = $stackPtr;
}
}//end if
if (isset($pattern[($i + 1)]) === true
&& $pattern[($i + 1)]['type'] === 'skip'
) {
// The next token is a skip token, so we just need to make
// sure the whitespace we found has *at least* the
// whitespace required.
if (strpos($tokenContent, $pattern[$i]['value']) !== 0) {
$hasError = true;
}
} else {
if ($tokenContent !== $pattern[$i]['value']) {
$hasError = true;
}
}
} else {
// Check to see if this important token is the same as the
// next important token in the pattern. If it is not, then
// the pattern cannot be for this piece of code.
$next = $phpcsFile->findNext(
$ignoreTokens,
$stackPtr,
null,
true
);
if ($next === false
|| $tokens[$next]['code'] !== $pattern[$i]['token']
) {
// The next important token did not match the pattern.
return false;
}
if ($lastAddedStackPtr !== null) {
if (($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET
|| $tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET)
&& isset($tokens[$next]['scope_condition']) === true
&& $tokens[$next]['scope_condition'] > $lastAddedStackPtr
) {
// This is a brace, but the owner of it is after the current
// token, which means it does not belong to any token in
// our pattern. This means the pattern is not for us.
return false;
}
if (($tokens[$next]['code'] === T_OPEN_PARENTHESIS
|| $tokens[$next]['code'] === T_CLOSE_PARENTHESIS)
&& isset($tokens[$next]['parenthesis_owner']) === true
&& $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr
) {
// This is a bracket, but the owner of it is after the current
// token, which means it does not belong to any token in
// our pattern. This means the pattern is not for us.
return false;
}
}//end if
// If we skipped past some whitespace tokens, then add them
// to the found string.
if (($next - $stackPtr) > 0) {
$hasComment = false;
for ($j = $stackPtr; $j < $next; $j++) {
$found .= $tokens[$j]['content'];
if (isset(Tokens::$commentTokens[$tokens[$j]['code']]) === true) {
$hasComment = true;
}
}
// If we are not ignoring comments, this additional
// whitespace or comment is not allowed. If we are
// ignoring comments, there needs to be at least one
// comment for this to be allowed.
if ($this->ignoreComments === false
|| ($this->ignoreComments === true
&& $hasComment === false)
) {
$hasError = true;
}
// Even when ignoring comments, we are not allowed to include
// newlines without the pattern specifying them, so
// everything should be on the same line.
if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) {
$hasError = true;
}
}//end if
if ($next !== $lastAddedStackPtr) {
$found .= $tokens[$next]['content'];
$lastAddedStackPtr = $next;
}
if (isset($pattern[($i + 1)]) === true
&& $pattern[($i + 1)]['type'] === 'skip'
) {
$stackPtr = $next;
} else {
$stackPtr = ($next + 1);
}
}//end if
} else if ($pattern[$i]['type'] === 'skip') {
if ($pattern[$i]['to'] === 'unknown') {
$next = $phpcsFile->findNext(
$pattern[($i + 1)]['token'],
$stackPtr
);
if ($next === false) {
// Couldn't find the next token, so we must
// be using the wrong pattern.
return false;
}
$found .= '...';
$stackPtr = $next;
} else {
// Find the previous opener.
$next = $phpcsFile->findPrevious(
Tokens::$blockOpeners,
$stackPtr
);
if ($next === false
|| isset($tokens[$next][$pattern[$i]['to']]) === false
) {
// If there was not opener, then we must
// be using the wrong pattern.
return false;
}
$found .= '...';
if ($pattern[$i]['to'] === 'parenthesis_closer') {
$found .= ')';
} else {
$found .= '}';
}
// Skip to the closing token.
$stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
}//end if
} else if ($pattern[$i]['type'] === 'string') {
if ($tokens[$stackPtr]['code'] !== T_STRING) {
$hasError = true;
}
if ($stackPtr !== $lastAddedStackPtr) {
$found .= 'abc';
$lastAddedStackPtr = $stackPtr;
}
$stackPtr++;
} else if ($pattern[$i]['type'] === 'newline') {
// Find the next token that contains a newline character.
$newline = 0;
for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++) {
if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) {
$newline = $j;
break;
}
}
if ($newline === 0) {
// We didn't find a newline character in the rest of the file.
$next = ($phpcsFile->numTokens - 1);
$hasError = true;
} else {
if ($this->ignoreComments === false) {
// The newline character cannot be part of a comment.
if (isset(Tokens::$commentTokens[$tokens[$newline]['code']]) === true) {
$hasError = true;
}
}
if ($newline === $stackPtr) {
$next = ($stackPtr + 1);
} else {
// Check that there were no significant tokens that we
// skipped over to find our newline character.
$next = $phpcsFile->findNext(
$ignoreTokens,
$stackPtr,
null,
true
);
if ($next < $newline) {
// We skipped a non-ignored token.
$hasError = true;
} else {
$next = ($newline + 1);
}
}
}//end if
if ($stackPtr !== $lastAddedStackPtr) {
$found .= $phpcsFile->getTokensAsString(
$stackPtr,
($next - $stackPtr)
);
$lastAddedStackPtr = ($next - 1);
}
$stackPtr = $next;
}//end if
}//end for
if ($hasError === true) {
$error = $this->prepareError($found, $patternCode);
$errors[$origStackPtr] = $error;
}
return $errors;
}//end processPattern()
/**
* Prepares an error for the specified patternCode.
*
* @param string $found The actual found string in the code.
* @param string $patternCode The expected pattern code.
*
* @return string The error message.
*/
protected function prepareError($found, $patternCode)
{
$found = str_replace("\r\n", '\n', $found);
$found = str_replace("\n", '\n', $found);
$found = str_replace("\r", '\n', $found);
$found = str_replace("\t", '\t', $found);
$found = str_replace('EOL', '\n', $found);
$expected = str_replace('EOL', '\n', $patternCode);
$error = "Expected \"$expected\"; found \"$found\"";
return $error;
}//end prepareError()
/**
* Returns the patterns that should be checked.
*
* @return string[]
*/
abstract protected function getPatterns();
/**
* Registers any supplementary tokens that this test might wish to process.
*
* A sniff may wish to register supplementary tests when it wishes to group
* an arbitrary validation that cannot be performed using a pattern, with
* other pattern tests.
*
* @return int[]
* @see processSupplementary()
*/
protected function registerSupplementary()
{
return [];
}//end registerSupplementary()
/**
* Processes any tokens registered with registerSupplementary().
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where to
* process the skip.
* @param int $stackPtr The position in the tokens stack to
* process.
*
* @return void
* @see registerSupplementary()
*/
protected function processSupplementary(File $phpcsFile, $stackPtr)
{
}//end processSupplementary()
/**
* Parses a pattern string into an array of pattern steps.
*
* @param string $pattern The pattern to parse.
*
* @return array The parsed pattern array.
* @see createSkipPattern()
* @see createTokenPattern()
*/
private function parse($pattern)
{
$patterns = [];
$length = strlen($pattern);
$lastToken = 0;
$firstToken = 0;
for ($i = 0; $i < $length; $i++) {
$specialPattern = false;
$isLastChar = ($i === ($length - 1));
$oldFirstToken = $firstToken;
if (substr($pattern, $i, 3) === '...') {
// It's a skip pattern. The skip pattern requires the
// content of the token in the "from" position and the token
// to skip to.
$specialPattern = $this->createSkipPattern($pattern, ($i - 1));
$lastToken = ($i - $firstToken);
$firstToken = ($i + 3);
$i += 2;
if ($specialPattern['to'] !== 'unknown') {
$firstToken++;
}
} else if (substr($pattern, $i, 3) === 'abc') {
$specialPattern = ['type' => 'string'];
$lastToken = ($i - $firstToken);
$firstToken = ($i + 3);
$i += 2;
} else if (substr($pattern, $i, 3) === 'EOL') {
$specialPattern = ['type' => 'newline'];
$lastToken = ($i - $firstToken);
$firstToken = ($i + 3);
$i += 2;
}//end if
if ($specialPattern !== false || $isLastChar === true) {
// If we are at the end of the string, don't worry about a limit.
if ($isLastChar === true) {
// Get the string from the end of the last skip pattern, if any,
// to the end of the pattern string.
$str = substr($pattern, $oldFirstToken);
} else {
// Get the string from the end of the last special pattern,
// if any, to the start of this special pattern.
if ($lastToken === 0) {
// Note that if the last special token was zero characters ago,
// there will be nothing to process so we can skip this bit.
// This happens if you have something like: EOL... in your pattern.
$str = '';
} else {
$str = substr($pattern, $oldFirstToken, $lastToken);
}
}
if ($str !== '') {
$tokenPatterns = $this->createTokenPattern($str);
foreach ($tokenPatterns as $tokenPattern) {
$patterns[] = $tokenPattern;
}
}
// Make sure we don't skip the last token.
if ($isLastChar === false && $i === ($length - 1)) {
$i--;
}
}//end if
// Add the skip pattern *after* we have processed
// all the tokens from the end of the last skip pattern
// to the start of this skip pattern.
if ($specialPattern !== false) {
$patterns[] = $specialPattern;
}
}//end for
return $patterns;
}//end parse()
/**
* Creates a skip pattern.
*
* @param string $pattern The pattern being parsed.
* @param int $from The token position that the skip pattern starts from.
*
* @return array The pattern step.
* @see createTokenPattern()
* @see parse()
*/
private function createSkipPattern($pattern, $from)
{
$skip = ['type' => 'skip'];
$nestedParenthesis = 0;
$nestedBraces = 0;
for ($start = $from; $start >= 0; $start--) {
switch ($pattern[$start]) {
case '(':
if ($nestedParenthesis === 0) {
$skip['to'] = 'parenthesis_closer';
}
$nestedParenthesis--;
break;
case '{':
if ($nestedBraces === 0) {
$skip['to'] = 'scope_closer';
}
$nestedBraces--;
break;
case '}':
$nestedBraces++;
break;
case ')':
$nestedParenthesis++;
break;
}//end switch
if (isset($skip['to']) === true) {
break;
}
}//end for
if (isset($skip['to']) === false) {
$skip['to'] = 'unknown';
}
return $skip;
}//end createSkipPattern()
/**
* Creates a token pattern.
*
* @param string $str The tokens string that the pattern should match.
*
* @return array The pattern step.
* @see createSkipPattern()
* @see parse()
*/
private function createTokenPattern($str)
{
// Don't add a space after the closing php tag as it will add a new
// whitespace token.
$tokenizer = new PHP('<?php '.$str.'?>', null);
// Remove the <?php tag from the front and the end php tag from the back.
$tokens = $tokenizer->getTokens();
$tokens = array_slice($tokens, 1, (count($tokens) - 2));
$patterns = [];
foreach ($tokens as $patternInfo) {
$patterns[] = [
'type' => 'token',
'token' => $patternInfo['code'],
'value' => $patternInfo['content'],
];
}
return $patterns;
}//end createTokenPattern()
}//end class
@@ -0,0 +1,189 @@
<?php
/**
* Allows tests that extend this class to listen for tokens within a particular scope.
*
* Below is a test that listens to methods that exist only within classes:
* <code>
* class ClassScopeTest extends PHP_CodeSniffer_Standards_AbstractScopeSniff
* {
* public function __construct()
* {
* parent::__construct(array(T_CLASS), array(T_FUNCTION));
* }
*
* protected function processTokenWithinScope(\PHP_CodeSniffer\Files\File $phpcsFile, $stackPtr, $currScope)
* {
* $className = $phpcsFile->getDeclarationName($currScope);
* echo 'encountered a method within class '.$className;
* }
* }
* </code>
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Sniffs;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
abstract class AbstractScopeSniff implements Sniff
{
/**
* The token types that this test wishes to listen to within the scope.
*
* @var array
*/
private $tokens = [];
/**
* The type of scope opener tokens that this test wishes to listen to.
*
* @var array<int|string>
*/
private $scopeTokens = [];
/**
* True if this test should fire on tokens outside of the scope.
*
* @var boolean
*/
private $listenOutside = false;
/**
* Constructs a new AbstractScopeTest.
*
* @param array $scopeTokens The type of scope the test wishes to listen to.
* @param array $tokens The tokens that the test wishes to listen to
* within the scope.
* @param boolean $listenOutside If true this test will also alert the
* extending class when a token is found outside
* the scope, by calling the
* processTokenOutsideScope method.
*
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified tokens arrays are empty
* or invalid.
*/
public function __construct(
array $scopeTokens,
array $tokens,
$listenOutside=false
) {
if (empty($scopeTokens) === true) {
$error = 'The scope tokens list cannot be empty';
throw new RuntimeException($error);
}
if (empty($tokens) === true) {
$error = 'The tokens list cannot be empty';
throw new RuntimeException($error);
}
$invalidScopeTokens = array_intersect($scopeTokens, $tokens);
if (empty($invalidScopeTokens) === false) {
$invalid = implode(', ', $invalidScopeTokens);
$error = "Scope tokens [$invalid] can't be in the tokens array";
throw new RuntimeException($error);
}
$this->listenOutside = $listenOutside;
$this->scopeTokens = array_flip($scopeTokens);
$this->tokens = $tokens;
}//end __construct()
/**
* The method that is called to register the tokens this test wishes to
* listen to.
*
* DO NOT OVERRIDE THIS METHOD. Use the constructor of this class to register
* for the desired tokens and scope.
*
* @return array<int|string>
* @see __constructor()
*/
final public function register()
{
return $this->tokens;
}//end register()
/**
* Processes the tokens that this test is listening for.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position in the stack where this
* token was found.
*
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached. Return `$phpcsFile->numTokens` to skip
* the rest of the file.
* @see processTokenWithinScope()
*/
final public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$foundScope = false;
$skipTokens = [];
foreach ($tokens[$stackPtr]['conditions'] as $scope => $code) {
if (isset($this->scopeTokens[$code]) === true) {
$skipTokens[] = $this->processTokenWithinScope($phpcsFile, $stackPtr, $scope);
$foundScope = true;
}
}
if ($this->listenOutside === true && $foundScope === false) {
$skipTokens[] = $this->processTokenOutsideScope($phpcsFile, $stackPtr);
}
if (empty($skipTokens) === false) {
return min($skipTokens);
}
}//end process()
/**
* Processes a token that is found within the scope that this test is
* listening to.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position in the stack where this
* token was found.
* @param int $currScope The position in the tokens array that
* opened the scope that this test is
* listening for.
*
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached. Return `$phpcsFile->numTokens` to skip
* the rest of the file.
*/
abstract protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope);
/**
* Processes a token that is found outside the scope that this test is
* listening to.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position in the stack where this
* token was found.
*
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached. Return `$phpcsFile->numTokens` to skip
* the rest of the file.
*/
abstract protected function processTokenOutsideScope(File $phpcsFile, $stackPtr);
}//end class
@@ -0,0 +1,230 @@
<?php
/**
* A class to find T_VARIABLE tokens.
*
* This class can distinguish between normal T_VARIABLE tokens, and those tokens
* that represent class members. If a class member is encountered, then the
* processMemberVar method is called so the extending class can process it. If
* the token is found to be a normal T_VARIABLE token, then processVariable is
* called.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Sniffs;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
abstract class AbstractVariableSniff extends AbstractScopeSniff
{
/**
* List of PHP Reserved variables.
*
* Used by various naming convention sniffs.
*
* @var array
*/
protected $phpReservedVars = [
'_SERVER' => true,
'_GET' => true,
'_POST' => true,
'_REQUEST' => true,
'_SESSION' => true,
'_ENV' => true,
'_COOKIE' => true,
'_FILES' => true,
'GLOBALS' => true,
'http_response_header' => true,
'HTTP_RAW_POST_DATA' => true,
'php_errormsg' => true,
];
/**
* Constructs an AbstractVariableTest.
*/
public function __construct()
{
$scopes = Tokens::$ooScopeTokens;
$listen = [
T_VARIABLE,
T_DOUBLE_QUOTED_STRING,
T_HEREDOC,
];
parent::__construct($scopes, $listen, true);
}//end __construct()
/**
* Processes the token in the specified PHP_CodeSniffer\Files\File.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this
* token was found.
* @param int $stackPtr The position where the token was found.
* @param int $currScope The current scope opener token.
*
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached. Return `$phpcsFile->numTokens` to skip
* the rest of the file.
*/
final protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope)
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_DOUBLE_QUOTED_STRING
|| $tokens[$stackPtr]['code'] === T_HEREDOC
) {
// Check to see if this string has a variable in it.
$pattern = '|(?<!\\\\)(?:\\\\{2})*\${?[a-zA-Z0-9_]+}?|';
if (preg_match($pattern, $tokens[$stackPtr]['content']) !== 0) {
return $this->processVariableInString($phpcsFile, $stackPtr);
}
return;
}
// If this token is nested inside a function at a deeper
// level than the current OO scope that was found, it's a normal
// variable and not a member var.
$conditions = array_reverse($tokens[$stackPtr]['conditions'], true);
$inFunction = false;
foreach ($conditions as $scope => $code) {
if (isset(Tokens::$ooScopeTokens[$code]) === true) {
break;
}
if ($code === T_FUNCTION || $code === T_CLOSURE) {
$inFunction = true;
}
}
if ($scope !== $currScope) {
// We found a closer scope to this token, so ignore
// this particular time through the sniff. We will process
// this token when this closer scope is found to avoid
// duplicate checks.
return;
}
// Just make sure this isn't a variable in a function declaration.
if ($inFunction === false && isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
foreach ($tokens[$stackPtr]['nested_parenthesis'] as $opener => $closer) {
if (isset($tokens[$opener]['parenthesis_owner']) === false) {
// Check if this is a USE statement for a closure.
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true);
if ($tokens[$prev]['code'] === T_USE) {
$inFunction = true;
break;
}
continue;
}
$owner = $tokens[$opener]['parenthesis_owner'];
if ($tokens[$owner]['code'] === T_FUNCTION
|| $tokens[$owner]['code'] === T_CLOSURE
) {
$inFunction = true;
break;
}
}
}//end if
if ($inFunction === true) {
return $this->processVariable($phpcsFile, $stackPtr);
} else {
return $this->processMemberVar($phpcsFile, $stackPtr);
}
}//end processTokenWithinScope()
/**
* Processes the token outside the scope in the file.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this
* token was found.
* @param int $stackPtr The position where the token was found.
*
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached. Return `$phpcsFile->numTokens` to skip
* the rest of the file.
*/
final protected function processTokenOutsideScope(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// These variables are not member vars.
if ($tokens[$stackPtr]['code'] === T_VARIABLE) {
return $this->processVariable($phpcsFile, $stackPtr);
} else if ($tokens[$stackPtr]['code'] === T_DOUBLE_QUOTED_STRING
|| $tokens[$stackPtr]['code'] === T_HEREDOC
) {
// Check to see if this string has a variable in it.
$pattern = '|(?<!\\\\)(?:\\\\{2})*\${?[a-zA-Z0-9_]+}?|';
if (preg_match($pattern, $tokens[$stackPtr]['content']) !== 0) {
return $this->processVariableInString($phpcsFile, $stackPtr);
}
}
}//end processTokenOutsideScope()
/**
* Called to process class member vars.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this
* token was found.
* @param int $stackPtr The position where the token was found.
*
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached. Return `$phpcsFile->numTokens` to skip
* the rest of the file.
*/
abstract protected function processMemberVar(File $phpcsFile, $stackPtr);
/**
* Called to process normal member vars.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this
* token was found.
* @param int $stackPtr The position where the token was found.
*
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached. Return `$phpcsFile->numTokens` to skip
* the rest of the file.
*/
abstract protected function processVariable(File $phpcsFile, $stackPtr);
/**
* Called to process variables found in double quoted strings or heredocs.
*
* Note that there may be more than one variable in the string, which will
* result only in one call for the string or one call per line for heredocs.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this
* token was found.
* @param int $stackPtr The position where the double quoted
* string was found.
*
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached. Return `$phpcsFile->numTokens` to skip
* the rest of the file.
*/
abstract protected function processVariableInString(File $phpcsFile, $stackPtr);
}//end class
@@ -0,0 +1,80 @@
<?php
/**
* Represents a PHP_CodeSniffer sniff for sniffing coding standards.
*
* A sniff registers what token types it wishes to listen for, then, when
* PHP_CodeSniffer encounters that token, the sniff is invoked and passed
* information about where the token was found in the stack, and the
* PHP_CodeSniffer file in which the token was found.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Sniffs;
use PHP_CodeSniffer\Files\File;
interface Sniff
{
/**
* Registers the tokens that this sniff wants to listen for.
*
* An example return value for a sniff that wants to listen for whitespace
* and any comments would be:
*
* <code>
* return array(
* T_WHITESPACE,
* T_DOC_COMMENT,
* T_COMMENT,
* );
* </code>
*
* @return array<int|string>
* @see Tokens.php
*/
public function register();
/**
* Called when one of the token types that this sniff is listening for
* is found.
*
* The stackPtr variable indicates where in the stack the token was found.
* A sniff can acquire information about this token, along with all the other
* tokens within the stack by first acquiring the token stack:
*
* <code>
* $tokens = $phpcsFile->getTokens();
* echo 'Encountered a '.$tokens[$stackPtr]['type'].' token';
* echo 'token information: ';
* print_r($tokens[$stackPtr]);
* </code>
*
* If the sniff discovers an anomaly in the code, they can raise an error
* by calling addError() on the \PHP_CodeSniffer\Files\File object, specifying an error
* message and the position of the offending token:
*
* <code>
* $phpcsFile->addError('Encountered an error', $stackPtr);
* </code>
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position in the PHP_CodeSniffer
* file's token stack where the token
* was found.
*
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached. Return `$phpcsFile->numTokens` to skip
* the rest of the file.
*/
public function process(File $phpcsFile, $stackPtr);
}//end interface
@@ -0,0 +1,44 @@
<documentation title="Empty PHP Statement">
<standard>
<![CDATA[
Empty PHP tags are not allowed.
]]>
</standard>
<code_comparison>
<code title="Valid: There is at least one statement inside the PHP tag pair.">
<![CDATA[
<?php <em>echo 'Hello World';</em> ?>
<?= <em>'Hello World';</em> ?>
]]>
</code>
<code title="Invalid: There is no statement inside the PHP tag pair.">
<![CDATA[
<?php <em>;</em> ?>
<?= <em></em> ?>
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
Superfluous semicolons are not allowed.
]]>
</standard>
<code_comparison>
<code title="Valid: There is no superfluous semicolon after a PHP statement.">
<![CDATA[
function_call()<em>;</em>
if (true) {
echo 'Hello World'<em>;</em>
}
]]>
</code>
<code title="Invalid: There are one or more superfluous semicolons after a PHP statement.">
<![CDATA[
function_call()<em>;;;</em>
if (true) {
echo 'Hello World';
}<em>;</em>
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,23 @@
<documentation title="Disallow Yoda conditions">
<standard>
<![CDATA[
Yoda conditions are disallowed.
]]>
</standard>
<code_comparison>
<code title="Valid: Value to be asserted must go on the right side of the comparison.">
<![CDATA[
if ($test === null) <em>{</em>
$var = 1;
<em>}</em>
]]>
</code>
<code title="Invalid: Value to be asserted must not be on the left.">
<![CDATA[
if (null === $test) <em>{</em>
$var = 1;
<em>}</em>
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,24 @@
<documentation title="Inline HTML">
<standard>
<![CDATA[
Files that contain PHP code should only have PHP code and should not have any "inline html".
]]>
</standard>
<code_comparison>
<code title="Valid: A PHP file with only PHP code in it.">
<![CDATA[
<?php
$foo = 'bar';
echo $foo . 'baz';
]]>
</code>
<code title="Invalid: A PHP file with html in it outside of the PHP tags.">
<![CDATA[
<em>some string here</em>
<?php
$foo = 'bar';
echo $foo . 'baz';
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,56 @@
<documentation title="Aligning Blocks of Assignments">
<standard>
<![CDATA[
There should be one space on either side of an equals sign used to assign a value to a variable. In the case of a block of related assignments, more space may be inserted to promote readability.
]]>
</standard>
<code_comparison>
<code title="Valid: Equals signs aligned.">
<![CDATA[
$shortVar <em>=</em> (1 + 2);
$veryLongVarName <em>=</em> 'string';
$var <em>=</em> foo($bar, $baz);
]]>
</code>
<code title="Invalid: Not aligned; harder to read.">
<![CDATA[
$shortVar <em>=</em> (1 + 2);
$veryLongVarName <em>=</em> 'string';
$var <em>=</em> foo($bar, $baz);
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
When using plus-equals, minus-equals etc. still ensure the equals signs are aligned to one space after the longest variable name.
]]>
</standard>
<code_comparison>
<code title="Valid: Equals signs aligned; only one space after longest var name.">
<![CDATA[
$shortVar <em>+= </em>1;
$veryLongVarName<em> = </em>1;
]]>
</code>
<code title="Invalid: Two spaces after longest var name.">
<![CDATA[
$shortVar <em> += </em>1;
$veryLongVarName<em> = </em>1;
]]>
</code>
</code_comparison>
<code_comparison>
<code title="Valid: Equals signs aligned.">
<![CDATA[
$shortVar <em> = </em>1;
$veryLongVarName<em> -= </em>1;
]]>
</code>
<code title="Invalid: Equals signs not aligned.">
<![CDATA[
$shortVar <em> = </em>1;
$veryLongVarName<em> -= </em>1;
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,19 @@
<documentation title="No Space After Cast">
<standard>
<![CDATA[
Spaces are not allowed after casting operators.
]]>
</standard>
<code_comparison>
<code title="Valid: A cast operator is immediately before its value.">
<![CDATA[
$foo = (string)1;
]]>
</code>
<code title="Invalid: A cast operator is followed by whitespace.">
<![CDATA[
$foo = (string)<em> </em>1;
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,19 @@
<documentation title="Space After Cast">
<standard>
<![CDATA[
Exactly one space is allowed after a cast.
]]>
</standard>
<code_comparison>
<code title="Valid: A cast operator is followed by one space.">
<![CDATA[
$foo = (string)<em> </em>1;
]]>
</code>
<code title="Invalid: A cast operator is not followed by whitespace.">
<![CDATA[
$foo = (string)<em></em>1;
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,24 @@
<documentation title="Opening Brace in Function Declarations">
<standard>
<![CDATA[
Function declarations follow the "BSD/Allman style". The function brace is on the line following the function declaration and is indented to the same column as the start of the function declaration.
]]>
</standard>
<code_comparison>
<code title="Valid: Brace on next line.">
<![CDATA[
function fooFunction($arg1, $arg2 = '')
<em>{</em>
...
}
]]>
</code>
<code title="Invalid: Brace on same line.">
<![CDATA[
function fooFunction($arg1, $arg2 = '') <em>{</em>
...
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,24 @@
<documentation title="Opening Brace in Function Declarations">
<standard>
<![CDATA[
Function declarations follow the "Kernighan/Ritchie style". The function brace is on the same line as the function declaration. One space is required between the closing parenthesis and the brace.
]]>
</standard>
<code_comparison>
<code title="Valid: Brace on same line.">
<![CDATA[
function fooFunction($arg1, $arg2 = '')<em> {</em>
...
}
]]>
</code>
<code title="Invalid: Brace on next line.">
<![CDATA[
function fooFunction($arg1, $arg2 = '')
<em>{</em>
...
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,23 @@
<documentation title="Abstract class name">
<standard>
<![CDATA[
Abstract class names must be prefixed with "Abstract", e.g. AbstractBar.
]]>
</standard>
<code_comparison>
<code title="Valid: Class name starts with 'Abstract'.">
<![CDATA[
abstract class <em>AbstractBar</em>
{
}
]]>
</code>
<code title="Invalid: Class name does not start with 'Abstract'.">
<![CDATA[
abstract class <em>Bar</em>
{
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,23 @@
<documentation title="Interface name suffix">
<standard>
<![CDATA[
Interface names must be suffixed with "Interface", e.g. BarInterface.
]]>
</standard>
<code_comparison>
<code title="Valid: Interface name ends on 'Interface'.">
<![CDATA[
interface <em>BarInterface</em>
{
}
]]>
</code>
<code title="Invalid: Interface name does not end on 'Interface'.">
<![CDATA[
interface <em>Bar</em>
{
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,23 @@
<documentation title="Trait name suffix">
<standard>
<![CDATA[
Trait names must be suffixed with "Trait", e.g. BarTrait.
]]>
</standard>
<code_comparison>
<code title="Valid: Trait name ends on 'Trait'.">
<![CDATA[
trait <em>BarTrait</em>
{
}
]]>
</code>
<code title="Invalid: Trait name does not end on 'Trait'.">
<![CDATA[
trait <em>Bar</em>
{
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,29 @@
<documentation title="Constant Names">
<standard>
<![CDATA[
Constants should always be all-uppercase, with underscores to separate words.
]]>
</standard>
<code_comparison>
<code title="Valid: All uppercase constant name.">
<![CDATA[
define('<em>FOO_CONSTANT</em>', 'foo');
class FooClass
{
const <em>FOO_CONSTANT</em> = 'foo';
}
]]>
</code>
<code title="Invalid: Mixed case or lowercase constant name.">
<![CDATA[
define('<em>Foo_Constant</em>', 'foo');
class FooClass
{
const <em>foo_constant</em> = 'foo';
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,22 @@
<documentation title="Opening Tag at Start of File">
<standard>
<![CDATA[
The opening PHP tag should be the first item in the file.
]]>
</standard>
<code_comparison>
<code title="Valid: A file starting with an opening PHP tag.">
<![CDATA[
<em></em><?php
echo 'Foo';
]]>
</code>
<code title="Invalid: A file with content before the opening PHP tag.">
<![CDATA[
<em>Beginning content</em>
<?php
echo 'Foo';
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,22 @@
<documentation title="Closing PHP Tags">
<standard>
<![CDATA[
All opening PHP tags should have a corresponding closing tag.
]]>
</standard>
<code_comparison>
<code title="Valid: A closing tag paired with its opening tag.">
<![CDATA[
<em><?php</em>
echo 'Foo';
<em>?></em>
]]>
</code>
<code title="Invalid: No closing tag paired with the opening tag.">
<![CDATA[
<em><?php</em>
echo 'Foo';
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,7 @@
<documentation title="$_REQUEST Superglobal">
<standard>
<![CDATA[
$_REQUEST should never be used due to the ambiguity created as to where the data is coming from. Use $_POST, $_GET, or $_COOKIE instead.
]]>
</standard>
</documentation>
@@ -0,0 +1,23 @@
<documentation title="Lowercase PHP Constants">
<standard>
<![CDATA[
The <em>true</em>, <em>false</em> and <em>null</em> constants must always be lowercase.
]]>
</standard>
<code_comparison>
<code title="Valid: Lowercase constants.">
<![CDATA[
if ($var === <em>false</em> || $var === <em>null</em>) {
$var = <em>true</em>;
}
]]>
</code>
<code title="Invalid: Uppercase constants.">
<![CDATA[
if ($var === <em>FALSE</em> || $var === <em>NULL</em>) {
$var = <em>TRUE</em>;
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,23 @@
<documentation title="SAPI Usage">
<standard>
<![CDATA[
The PHP_SAPI constant should be used instead of php_sapi_name().
]]>
</standard>
<code_comparison>
<code title="Valid: PHP_SAPI is used.">
<![CDATA[
if (<em>PHP_SAPI</em> === 'cli') {
echo "Hello, CLI user.";
}
]]>
</code>
<code title="Invalid: Function call to php_sapi_name() is used.">
<![CDATA[
if (<em>php_sapi_name()</em> === 'cli') {
echo "Hello, CLI user.";
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,23 @@
<documentation title="Uppercase PHP Constants">
<standard>
<![CDATA[
The <em>true</em>, <em>false</em> and <em>null</em> constants must always be uppercase.
]]>
</standard>
<code_comparison>
<code title="Valid: Uppercase constants.">
<![CDATA[
if ($var === <em>FALSE</em> || $var === <em>NULL</em>) {
$var = <em>TRUE</em>;
}
]]>
</code>
<code title="Invalid: Lowercase constants.">
<![CDATA[
if ($var === <em>false</em> || $var === <em>null</em>) {
$var = <em>true</em>;
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,7 @@
<documentation title="Subversion Properties">
<standard>
<![CDATA[
All PHP files in a subversion repository should have the svn:keywords property set to 'Author Id Revision' and the svn:eol-style property set to 'native'.
]]>
</standard>
</documentation>
@@ -0,0 +1,23 @@
<documentation title="Arbitrary Parentheses Spacing">
<standard>
<![CDATA[
Arbitrary sets of parentheses should have no spaces inside.
]]>
</standard>
<code_comparison>
<code title="Valid: No spaces on the inside of a set of arbitrary parentheses.">
<![CDATA[
$a = (null !== $extra);
]]>
</code>
<code title="Invalid: Spaces or new lines on the inside of a set of arbitrary parentheses.">
<![CDATA[
$a = ( null !== $extra );
$a = (
null !== $extra
);
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,34 @@
<documentation title="Spacing After Spread Operator">
<standard>
<![CDATA[
There should be no space between the spread operator and the variable/function call it applies to.
]]>
</standard>
<code_comparison>
<code title="Valid: No space between the spread operator and the variable/function call it applies to.">
<![CDATA[
function foo(<em>&...$spread</em>) {
bar(<em>...$spread</em>);
bar(
[<em>...$foo</em>],
<em>...array_values($keyedArray)</em>
);
}
]]>
</code>
<code title="Invalid: Space found between the spread operator and the variable/function call it applies to.">
<![CDATA[
function bar(<em>... </em>$spread) {
bar(<em>...
</em>$spread
);
bar(
[<em>... </em>$foo ],<em>.../*@*/</em>array_values($keyed)
);
}
]]>
</code>
</code_comparison>
</documentation>
@@ -0,0 +1,193 @@
<?php
/**
* Ensures that array are indented one tab stop.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Arrays;
use PHP_CodeSniffer\Sniffs\AbstractArraySniff;
use PHP_CodeSniffer\Util\Tokens;
class ArrayIndentSniff extends AbstractArraySniff
{
/**
* The number of spaces each array key should be indented.
*
* @var integer
*/
public $indent = 4;
/**
* Processes a single-line array definition.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param int $arrayStart The token that starts the array definition.
* @param int $arrayEnd The token that ends the array definition.
* @param array $indices An array of token positions for the array keys,
* double arrows, and values.
*
* @return void
*/
public function processSingleLineArray($phpcsFile, $stackPtr, $arrayStart, $arrayEnd, $indices)
{
}//end processSingleLineArray()
/**
* Processes a multi-line array definition.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param int $arrayStart The token that starts the array definition.
* @param int $arrayEnd The token that ends the array definition.
* @param array $indices An array of token positions for the array keys,
* double arrows, and values.
*
* @return void
*/
public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $arrayEnd, $indices)
{
$tokens = $phpcsFile->getTokens();
// Determine how far indented the entire array declaration should be.
$ignore = Tokens::$emptyTokens;
$ignore[] = T_DOUBLE_ARROW;
$prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
$start = $phpcsFile->findStartOfStatement($prev);
$first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $start, true);
$baseIndent = ($tokens[$first]['column'] - 1);
$first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr, true);
$startIndent = ($tokens[$first]['column'] - 1);
// If the open brace is not indented to at least to the level of the start
// of the statement, the sniff will conflict with other sniffs trying to
// check indent levels because it's not valid. But we don't enforce exactly
// how far indented it should be.
if ($startIndent < $baseIndent) {
$pluralizeSpace = 's';
if ($baseIndent === 1) {
$pluralizeSpace = '';
}
$error = 'Array open brace not indented correctly; expected at least %s space%s but found %s';
$data = [
$baseIndent,
$pluralizeSpace,
$startIndent,
];
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'OpenBraceIncorrect', $data);
if ($fix === true) {
$padding = str_repeat(' ', $baseIndent);
if ($startIndent === 0) {
$phpcsFile->fixer->addContentBefore($first, $padding);
} else {
$phpcsFile->fixer->replaceToken(($first - 1), $padding);
}
}
return;
}//end if
$expectedIndent = ($startIndent + $this->indent);
foreach ($indices as $index) {
if (isset($index['index_start']) === true) {
$start = $index['index_start'];
} else {
$start = $index['value_start'];
}
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($start - 1), null, true);
if ($tokens[$prev]['line'] === $tokens[$start]['line']) {
// This index isn't the only content on the line
// so we can't check indent rules.
continue;
}
$first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $start, true);
$foundIndent = ($tokens[$first]['column'] - 1);
if ($foundIndent === $expectedIndent) {
continue;
}
$pluralizeSpace = 's';
if ($expectedIndent === 1) {
$pluralizeSpace = '';
}
$error = 'Array key not indented correctly; expected %s space%s but found %s';
$data = [
$expectedIndent,
$pluralizeSpace,
$foundIndent,
];
$fix = $phpcsFile->addFixableError($error, $first, 'KeyIncorrect', $data);
if ($fix === false) {
continue;
}
$padding = str_repeat(' ', $expectedIndent);
if ($foundIndent === 0) {
$phpcsFile->fixer->addContentBefore($first, $padding);
} else {
$phpcsFile->fixer->replaceToken(($first - 1), $padding);
}
}//end foreach
$prev = $phpcsFile->findPrevious(T_WHITESPACE, ($arrayEnd - 1), null, true);
if ($tokens[$prev]['line'] === $tokens[$arrayEnd]['line']) {
$error = 'Closing brace of array declaration must be on a new line';
$fix = $phpcsFile->addFixableError($error, $arrayEnd, 'CloseBraceNotNewLine');
if ($fix === true) {
$padding = $phpcsFile->eolChar.str_repeat(' ', $startIndent);
$phpcsFile->fixer->addContentBefore($arrayEnd, $padding);
}
return;
}
// The close brace must be indented one stop less.
$foundIndent = ($tokens[$arrayEnd]['column'] - 1);
if ($foundIndent === $startIndent) {
return;
}
$pluralizeSpace = 's';
if ($startIndent === 1) {
$pluralizeSpace = '';
}
$error = 'Array close brace not indented correctly; expected %s space%s but found %s';
$data = [
$startIndent,
$pluralizeSpace,
$foundIndent,
];
$fix = $phpcsFile->addFixableError($error, $arrayEnd, 'CloseBraceIncorrect', $data);
if ($fix === false) {
return;
}
$padding = str_repeat(' ', $startIndent);
if ($foundIndent === 0) {
$phpcsFile->fixer->addContentBefore($arrayEnd, $padding);
} else {
$phpcsFile->fixer->replaceToken(($arrayEnd - 1), $padding);
}
}//end processMultiLineArray()
}//end class
@@ -0,0 +1,72 @@
<?php
/**
* Bans the use of the PHP long array syntax.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Arrays;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class DisallowLongArraySyntaxSniff implements Sniff
{
/**
* Registers the tokens that this sniff wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_ARRAY];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$phpcsFile->recordMetric($stackPtr, 'Short array syntax used', 'no');
$error = 'Short array syntax must be used to define arrays';
if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
// Live coding/parse error, just show the error, don't try and fix it.
$phpcsFile->addError($error, $stackPtr, 'Found');
return;
}
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'Found');
if ($fix === true) {
$opener = $tokens[$stackPtr]['parenthesis_opener'];
$closer = $tokens[$stackPtr]['parenthesis_closer'];
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->replaceToken($stackPtr, '');
$phpcsFile->fixer->replaceToken($opener, '[');
$phpcsFile->fixer->replaceToken($closer, ']');
$phpcsFile->fixer->endChangeset();
}
}//end process()
}//end class
@@ -0,0 +1,126 @@
<?php
/**
* Reports errors if the same class or interface name is used in multiple files.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Classes;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class DuplicateClassNameSniff implements Sniff
{
/**
* List of classes that have been found during checking.
*
* @var array
*/
protected $foundClasses = [];
/**
* Registers the tokens that this sniff wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_OPEN_TAG];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$namespace = '';
$findTokens = [
T_CLASS,
T_INTERFACE,
T_TRAIT,
T_ENUM,
T_NAMESPACE,
];
$stackPtr = $phpcsFile->findNext($findTokens, ($stackPtr + 1));
while ($stackPtr !== false) {
// Keep track of what namespace we are in.
if ($tokens[$stackPtr]['code'] === T_NAMESPACE) {
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($nextNonEmpty !== false
// Ignore namespace keyword used as operator.
&& $tokens[$nextNonEmpty]['code'] !== T_NS_SEPARATOR
) {
$namespace = '';
for ($i = $nextNonEmpty; $i < $phpcsFile->numTokens; $i++) {
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
continue;
}
if ($tokens[$i]['code'] !== T_STRING && $tokens[$i]['code'] !== T_NS_SEPARATOR) {
break;
}
$namespace .= $tokens[$i]['content'];
}
$stackPtr = $i;
}
} else {
$name = $phpcsFile->getDeclarationName($stackPtr);
if (empty($name) === false) {
if ($namespace !== '') {
$name = $namespace.'\\'.$name;
}
$compareName = strtolower($name);
if (isset($this->foundClasses[$compareName]) === true) {
$type = strtolower($tokens[$stackPtr]['content']);
$file = $this->foundClasses[$compareName]['file'];
$line = $this->foundClasses[$compareName]['line'];
$error = 'Duplicate %s name "%s" found; first defined in %s on line %s';
$data = [
$type,
$name,
$file,
$line,
];
$phpcsFile->addWarning($error, $stackPtr, 'Found', $data);
} else {
$this->foundClasses[$compareName] = [
'file' => $phpcsFile->getFilename(),
'line' => $tokens[$stackPtr]['line'],
];
}
}//end if
if (isset($tokens[$stackPtr]['scope_closer']) === true) {
$stackPtr = $tokens[$stackPtr]['scope_closer'];
}
}//end if
$stackPtr = $phpcsFile->findNext($findTokens, ($stackPtr + 1));
}//end while
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,183 @@
<?php
/**
* Checks against empty PHP statements.
*
* - Check against two semicolons with no executable code in between.
* - Check against an empty PHP open - close tag combination.
*
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
* @copyright 2017 Juliette Reinders Folmer. All rights reserved.
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class EmptyPHPStatementSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_SEMICOLON,
T_CLOSE_TAG,
];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_SEMICOLON) {
$this->processSemicolon($phpcsFile, $stackPtr);
} else {
$this->processCloseTag($phpcsFile, $stackPtr);
}
}//end process()
/**
* Detect `something();;`.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
private function processSemicolon(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($tokens[$prevNonEmpty]['code'] !== T_SEMICOLON
&& $tokens[$prevNonEmpty]['code'] !== T_OPEN_TAG
&& $tokens[$prevNonEmpty]['code'] !== T_OPEN_TAG_WITH_ECHO
) {
if (isset($tokens[$prevNonEmpty]['scope_condition']) === false) {
return;
}
if ($tokens[$prevNonEmpty]['scope_opener'] !== $prevNonEmpty
&& $tokens[$prevNonEmpty]['code'] !== T_CLOSE_CURLY_BRACKET
) {
return;
}
$scopeOwner = $tokens[$tokens[$prevNonEmpty]['scope_condition']]['code'];
if ($scopeOwner === T_CLOSURE || $scopeOwner === T_ANON_CLASS || $scopeOwner === T_MATCH) {
return;
}
// Else, it's something like `if (foo) {};` and the semicolon is not needed.
}
if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
$nested = $tokens[$stackPtr]['nested_parenthesis'];
$lastCloser = array_pop($nested);
if (isset($tokens[$lastCloser]['parenthesis_owner']) === true
&& $tokens[$tokens[$lastCloser]['parenthesis_owner']]['code'] === T_FOR
) {
// Empty for() condition.
return;
}
}
$fix = $phpcsFile->addFixableWarning(
'Empty PHP statement detected: superfluous semicolon.',
$stackPtr,
'SemicolonWithoutCodeDetected'
);
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
if ($tokens[$prevNonEmpty]['code'] === T_OPEN_TAG
|| $tokens[$prevNonEmpty]['code'] === T_OPEN_TAG_WITH_ECHO
) {
// Check for superfluous whitespace after the semicolon which should be
// removed as the `<?php ` open tag token already contains whitespace,
// either a space or a new line.
if ($tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) {
$replacement = str_replace(' ', '', $tokens[($stackPtr + 1)]['content']);
$phpcsFile->fixer->replaceToken(($stackPtr + 1), $replacement);
}
}
for ($i = $stackPtr; $i > $prevNonEmpty; $i--) {
if ($tokens[$i]['code'] !== T_SEMICOLON
&& $tokens[$i]['code'] !== T_WHITESPACE
) {
break;
}
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->endChangeset();
}//end if
}//end processSemicolon()
/**
* Detect `<?php ? >`.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
private function processCloseTag(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$prevNonEmpty = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if ($tokens[$prevNonEmpty]['code'] !== T_OPEN_TAG
&& $tokens[$prevNonEmpty]['code'] !== T_OPEN_TAG_WITH_ECHO
) {
return;
}
$fix = $phpcsFile->addFixableWarning(
'Empty PHP open/close tag combination detected.',
$prevNonEmpty,
'EmptyPHPOpenCloseTagsDetected'
);
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
for ($i = $prevNonEmpty; $i <= $stackPtr; $i++) {
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->endChangeset();
}
}//end processCloseTag()
}//end class
@@ -0,0 +1,134 @@
<?php
/**
* Detects incrementer jumbling in for loops.
*
* This rule is based on the PMD rule catalogue. The jumbling incrementer sniff
* detects the usage of one and the same incrementer into an outer and an inner
* loop. Even it is intended this is confusing code.
*
* <code>
* class Foo
* {
* public function bar($x)
* {
* for ($i = 0; $i < 10; $i++)
* {
* for ($k = 0; $k < 20; $i++)
* {
* echo 'Hello';
* }
* }
* }
* }
* </code>
*
* @author Manuel Pichler <mapi@manuel-pichler.de>
* @copyright 2007-2014 Manuel Pichler. All rights reserved.
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class JumbledIncrementerSniff implements Sniff
{
/**
* Registers the tokens that this sniff wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_FOR];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
// Skip for-loop without body.
if (isset($token['scope_opener']) === false) {
return;
}
// Find incrementers for outer loop.
$outer = $this->findIncrementers($tokens, $token);
// Skip if empty.
if (count($outer) === 0) {
return;
}
// Find nested for loops.
$start = ++$token['scope_opener'];
$end = --$token['scope_closer'];
for (; $start <= $end; ++$start) {
if ($tokens[$start]['code'] !== T_FOR) {
continue;
}
$inner = $this->findIncrementers($tokens, $tokens[$start]);
$diff = array_intersect($outer, $inner);
if (count($diff) !== 0) {
$error = 'Loop incrementer (%s) jumbling with inner loop';
$data = [implode(', ', $diff)];
$phpcsFile->addWarning($error, $stackPtr, 'Found', $data);
}
}
}//end process()
/**
* Get all used variables in the incrementer part of a for statement.
*
* @param array<int, array> $tokens Array with all code sniffer tokens.
* @param array<string, mixed> $token Current for loop token.
*
* @return string[] List of all found incrementer variables.
*/
protected function findIncrementers(array $tokens, array $token)
{
// Skip invalid statement.
if (isset($token['parenthesis_opener'], $token['parenthesis_closer']) === false) {
return [];
}
$start = ++$token['parenthesis_opener'];
$end = --$token['parenthesis_closer'];
$incrementers = [];
$semicolons = 0;
for ($next = $start; $next <= $end; ++$next) {
$code = $tokens[$next]['code'];
if ($code === T_SEMICOLON) {
++$semicolons;
} else if ($semicolons === 2 && $code === T_VARIABLE) {
$incrementers[] = $tokens[$next]['content'];
}
}
return $incrementers;
}//end findIncrementers()
}//end class
@@ -0,0 +1,112 @@
<?php
/**
* Forbid mixing different binary boolean operators within a single expression without making precedence
* clear using parentheses.
*
* <code>
* $one = false;
* $two = false;
* $three = true;
*
* $result = $one && $two || $three;
* $result3 = $one && !$two xor $three;
* </code>
*
* {@internal The unary `!` operator is not handled, because its high precedence matches its visuals of
* applying only to the sub-expression right next to it, making it unlikely that someone would
* misinterpret its precedence. Requiring parentheses around it would reduce the readability of
* expressions due to the additional characters, especially if multiple subexpressions / variables
* need to be negated.}
*
* Sister-sniff to the `Squiz.ControlStructures.InlineIfDeclaration` and
* `Squiz.Formatting.OperatorBracket.MissingBrackets` sniffs.
*
* @author Tim Duesterhus <duesterhus@woltlab.com>
* @copyright 2021-2023 WoltLab GmbH.
* @copyright 2024 PHPCSStandards and contributors
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class RequireExplicitBooleanOperatorPrecedenceSniff implements Sniff
{
/**
* Array of tokens this test searches for to find either a boolean
* operator or the start of the current (sub-)expression. Used for
* performance optimization purposes.
*
* @var array<int|string>
*/
private $searchTargets = [];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
$this->searchTargets = Tokens::$booleanOperators;
$this->searchTargets[T_INLINE_THEN] = T_INLINE_THEN;
$this->searchTargets[T_INLINE_ELSE] = T_INLINE_ELSE;
return Tokens::$booleanOperators;
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$start = $phpcsFile->findStartOfStatement($stackPtr);
$previous = $phpcsFile->findPrevious(
$this->searchTargets,
($stackPtr - 1),
$start,
false,
null,
true
);
if ($previous === false) {
// No token found.
return;
}
if ($tokens[$previous]['code'] === $tokens[$stackPtr]['code']) {
// Identical operator found.
return;
}
if (in_array($tokens[$previous]['code'], [T_INLINE_THEN, T_INLINE_ELSE], true) === true) {
// Beginning of the expression found for the ternary conditional operator.
return;
}
// We found a mismatching operator, thus we must report the error.
$error = 'Mixing different binary boolean operators within an expression';
$error .= ' without using parentheses to clarify precedence is not allowed.';
$phpcsFile->addError($error, $stackPtr, 'MissingParentheses');
}//end process()
}//end class
@@ -0,0 +1,184 @@
<?php
/**
* Detects unnecessary overridden methods that simply call their parent.
*
* This rule is based on the PMD rule catalogue. The Useless Overriding Method
* sniff detects the use of methods that only call their parent class's method
* with the same name and arguments. These methods are not required.
*
* <code>
* class FooBar {
* public function __construct($a, $b) {
* parent::__construct($a, $b);
* }
* }
* </code>
*
* @author Manuel Pichler <mapi@manuel-pichler.de>
* @copyright 2007-2014 Manuel Pichler. All rights reserved.
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class UselessOverridingMethodSniff implements Sniff
{
/**
* Object-Oriented scopes in which a call to parent::method() can exist.
*
* @var array<int|string, bool> Keys are the token constants, value is irrelevant.
*/
private $validOOScopes = [
T_CLASS => true,
T_ANON_CLASS => true,
T_TRAIT => true,
];
/**
* Registers the tokens that this sniff wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_FUNCTION];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
// Skip function without body.
if (isset($token['scope_opener'], $token['scope_closer']) === false) {
return;
}
$conditions = $token['conditions'];
$lastCondition = end($conditions);
// Skip functions that are not a method part of a class, anon class or trait.
if (isset($this->validOOScopes[$lastCondition]) === false) {
return;
}
// Get function name.
$methodName = $phpcsFile->getDeclarationName($stackPtr);
// Get all parameters from method signature.
$signature = [];
foreach ($phpcsFile->getMethodParameters($stackPtr) as $param) {
$signature[] = $param['name'];
}
$next = ++$token['scope_opener'];
$end = --$token['scope_closer'];
for (; $next <= $end; ++$next) {
$code = $tokens[$next]['code'];
if (isset(Tokens::$emptyTokens[$code]) === true) {
continue;
} else if ($code === T_RETURN) {
continue;
}
break;
}
// Any token except 'parent' indicates correct code.
if ($tokens[$next]['code'] !== T_PARENT) {
return;
}
// Find next non empty token index, should be double colon.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), null, true);
// Skip for invalid code.
if ($tokens[$next]['code'] !== T_DOUBLE_COLON) {
return;
}
// Find next non empty token index, should be the name of the method being called.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), null, true);
// Skip for invalid code or other method.
if (strcasecmp($tokens[$next]['content'], $methodName) !== 0) {
return;
}
// Find next non empty token index, should be the open parenthesis.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), null, true);
// Skip for invalid code.
if ($tokens[$next]['code'] !== T_OPEN_PARENTHESIS || isset($tokens[$next]['parenthesis_closer']) === false) {
return;
}
$parameters = [''];
$parenthesisCount = 1;
for (++$next; $next < $phpcsFile->numTokens; ++$next) {
$code = $tokens[$next]['code'];
if ($code === T_OPEN_PARENTHESIS) {
++$parenthesisCount;
} else if ($code === T_CLOSE_PARENTHESIS) {
--$parenthesisCount;
} else if ($parenthesisCount === 1 && $code === T_COMMA) {
$parameters[] = '';
} else if (isset(Tokens::$emptyTokens[$code]) === false) {
$parameters[(count($parameters) - 1)] .= $tokens[$next]['content'];
}
if ($parenthesisCount === 0) {
break;
}
}//end for
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), null, true);
if ($tokens[$next]['code'] !== T_SEMICOLON && $tokens[$next]['code'] !== T_CLOSE_TAG) {
return;
}
// This list deliberately does not include the `T_OPEN_TAG_WITH_ECHO` as that token implicitly is an echo statement, i.e. content.
$nonContent = Tokens::$emptyTokens;
$nonContent[T_OPEN_TAG] = T_OPEN_TAG;
$nonContent[T_CLOSE_TAG] = T_CLOSE_TAG;
// Check rest of the scope.
for (++$next; $next <= $end; ++$next) {
$code = $tokens[$next]['code'];
// Skip for any other content.
if (isset($nonContent[$code]) === false) {
return;
}
}
$parameters = array_map('trim', $parameters);
$parameters = array_filter($parameters);
if (count($parameters) === count($signature) && $parameters === $signature) {
$phpcsFile->addWarning('Possible useless method overriding detected', $stackPtr, 'Found');
}
}//end process()
}//end class
@@ -0,0 +1,185 @@
<?php
/**
* Ban the use of Yoda conditions.
*
* @author Mponos George <gmponos@gmail.com>
* @author Mark Scherer <username@example.com>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class DisallowYodaConditionsSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
$tokens = Tokens::$comparisonTokens;
unset($tokens[T_COALESCE]);
return $tokens;
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$previousIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
$relevantTokens = [
T_CLOSE_SHORT_ARRAY,
T_CLOSE_PARENTHESIS,
T_TRUE,
T_FALSE,
T_NULL,
T_LNUMBER,
T_DNUMBER,
T_CONSTANT_ENCAPSED_STRING,
];
if (in_array($tokens[$previousIndex]['code'], $relevantTokens, true) === false) {
return;
}
if ($tokens[$previousIndex]['code'] === T_CLOSE_SHORT_ARRAY) {
$previousIndex = $tokens[$previousIndex]['bracket_opener'];
if ($this->isArrayStatic($phpcsFile, $previousIndex) === false) {
return;
}
}
$prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($previousIndex - 1), null, true);
if (in_array($tokens[$prevIndex]['code'], Tokens::$arithmeticTokens, true) === true) {
return;
}
if ($tokens[$prevIndex]['code'] === T_STRING_CONCAT) {
return;
}
// Is it a parenthesis.
if ($tokens[$previousIndex]['code'] === T_CLOSE_PARENTHESIS) {
$beforeOpeningParenthesisIndex = $phpcsFile->findPrevious(
Tokens::$emptyTokens,
($tokens[$previousIndex]['parenthesis_opener'] - 1),
null,
true
);
if ($beforeOpeningParenthesisIndex === false || $tokens[$beforeOpeningParenthesisIndex]['code'] !== T_ARRAY) {
if ($tokens[$beforeOpeningParenthesisIndex]['code'] === T_STRING) {
return;
}
// If it is not an array check what is inside.
$found = $phpcsFile->findPrevious(
T_VARIABLE,
($previousIndex - 1),
$tokens[$previousIndex]['parenthesis_opener']
);
// If a variable exists, it is not Yoda.
if ($found !== false) {
return;
}
// If there is nothing inside the parenthesis, it is not a Yoda condition.
$opener = $tokens[$previousIndex]['parenthesis_opener'];
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($previousIndex - 1), ($opener + 1), true);
if ($prev === false) {
return;
}
} else if ($tokens[$beforeOpeningParenthesisIndex]['code'] === T_ARRAY
&& $this->isArrayStatic($phpcsFile, $beforeOpeningParenthesisIndex) === false
) {
return;
}//end if
}//end if
$phpcsFile->addError(
'Usage of Yoda conditions is not allowed; switch the expression order',
$stackPtr,
'Found'
);
}//end process()
/**
* Determines if an array is a static definition.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $arrayToken The position of the array token.
*
* @return bool
*/
public function isArrayStatic(File $phpcsFile, $arrayToken)
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$arrayToken]['code'] === T_OPEN_SHORT_ARRAY) {
$start = $arrayToken;
$end = $tokens[$arrayToken]['bracket_closer'];
} else if ($tokens[$arrayToken]['code'] === T_ARRAY) {
$start = $tokens[$arrayToken]['parenthesis_opener'];
$end = $tokens[$arrayToken]['parenthesis_closer'];
} else {
// Shouldn't be possible but may happen if external sniffs are using this method.
return true; // @codeCoverageIgnore
}
$staticTokens = Tokens::$emptyTokens;
$staticTokens += Tokens::$textStringTokens;
$staticTokens += Tokens::$assignmentTokens;
$staticTokens += Tokens::$equalityTokens;
$staticTokens += Tokens::$comparisonTokens;
$staticTokens += Tokens::$arithmeticTokens;
$staticTokens += Tokens::$operators;
$staticTokens += Tokens::$booleanOperators;
$staticTokens += Tokens::$castTokens;
$staticTokens += Tokens::$bracketTokens;
$staticTokens += [
T_DOUBLE_ARROW => T_DOUBLE_ARROW,
T_COMMA => T_COMMA,
T_TRUE => T_TRUE,
T_FALSE => T_FALSE,
];
for ($i = ($start + 1); $i < $end; $i++) {
if (isset($tokens[$i]['scope_closer']) === true) {
$i = $tokens[$i]['scope_closer'];
continue;
}
if (isset($staticTokens[$tokens[$i]['code']]) === false) {
return false;
}
}
return true;
}//end isArrayStatic()
}//end class
@@ -0,0 +1,366 @@
<?php
/**
* Verifies that inline control statements are not present.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\ControlStructures;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class InlineControlStructureSniff implements Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = [
'PHP',
'JS',
];
/**
* If true, an error will be thrown; otherwise a warning.
*
* @var boolean
*/
public $error = true;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_IF,
T_ELSE,
T_ELSEIF,
T_FOREACH,
T_WHILE,
T_DO,
T_SWITCH,
T_FOR,
];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void|int
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['scope_opener']) === true) {
$phpcsFile->recordMetric($stackPtr, 'Control structure defined inline', 'no');
return;
}
// Ignore the ELSE in ELSE IF. We'll process the IF part later.
if ($tokens[$stackPtr]['code'] === T_ELSE) {
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($tokens[$next]['code'] === T_IF) {
return;
}
}
if ($tokens[$stackPtr]['code'] === T_WHILE || $tokens[$stackPtr]['code'] === T_FOR) {
// This could be from a DO WHILE, which doesn't have an opening brace or a while/for without body.
if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) {
$afterParensCloser = $phpcsFile->findNext(Tokens::$emptyTokens, ($tokens[$stackPtr]['parenthesis_closer'] + 1), null, true);
if ($afterParensCloser === false) {
// Live coding.
return;
}
if ($tokens[$afterParensCloser]['code'] === T_SEMICOLON) {
$phpcsFile->recordMetric($stackPtr, 'Control structure defined inline', 'no');
return;
}
}
}//end if
if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false
&& $tokens[$stackPtr]['code'] !== T_ELSE
) {
if ($tokens[$stackPtr]['code'] !== T_DO) {
// Live coding or parse error.
return;
}
$nextWhile = $phpcsFile->findNext(T_WHILE, ($stackPtr + 1));
if ($nextWhile !== false
&& isset($tokens[$nextWhile]['parenthesis_opener'], $tokens[$nextWhile]['parenthesis_closer']) === false
) {
// Live coding or parse error.
return;
}
unset($nextWhile);
}
$start = $stackPtr;
if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) {
$start = $tokens[$stackPtr]['parenthesis_closer'];
}
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($start + 1), null, true);
if ($nextNonEmpty === false) {
// Live coding or parse error.
return;
}
if ($tokens[$nextNonEmpty]['code'] === T_OPEN_CURLY_BRACKET
|| $tokens[$nextNonEmpty]['code'] === T_COLON
) {
// T_CLOSE_CURLY_BRACKET missing, or alternative control structure with
// T_END... missing. Either live coding, parse error or end
// tag in short open tags and scan run with short_open_tag=Off.
// Bow out completely as any further detection will be unreliable
// and create incorrect fixes or cause fixer conflicts.
return $phpcsFile->numTokens;
}
unset($nextNonEmpty, $start);
// This is a control structure without an opening brace,
// so it is an inline statement.
if ($this->error === true) {
$fix = $phpcsFile->addFixableError('Inline control structures are not allowed', $stackPtr, 'NotAllowed');
} else {
$fix = $phpcsFile->addFixableWarning('Inline control structures are discouraged', $stackPtr, 'Discouraged');
}
$phpcsFile->recordMetric($stackPtr, 'Control structure defined inline', 'yes');
// Stop here if we are not fixing the error.
if ($fix !== true) {
return;
}
$phpcsFile->fixer->beginChangeset();
if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) {
$closer = $tokens[$stackPtr]['parenthesis_closer'];
} else {
$closer = $stackPtr;
}
if ($tokens[($closer + 1)]['code'] === T_WHITESPACE
|| $tokens[($closer + 1)]['code'] === T_SEMICOLON
) {
$phpcsFile->fixer->addContent($closer, ' {');
} else {
$phpcsFile->fixer->addContent($closer, ' { ');
}
$fixableScopeOpeners = $this->register();
$lastNonEmpty = $closer;
for ($end = ($closer + 1); $end < $phpcsFile->numTokens; $end++) {
if ($tokens[$end]['code'] === T_SEMICOLON) {
break;
}
if ($tokens[$end]['code'] === T_CLOSE_TAG) {
$end = $lastNonEmpty;
break;
}
if (in_array($tokens[$end]['code'], $fixableScopeOpeners, true) === true
&& isset($tokens[$end]['scope_opener']) === false
) {
// The best way to fix nested inline scopes is middle-out.
// So skip this one. It will be detected and fixed on a future loop.
$phpcsFile->fixer->rollbackChangeset();
return;
}
if (isset($tokens[$end]['scope_opener']) === true) {
$type = $tokens[$end]['code'];
$end = $tokens[$end]['scope_closer'];
if ($type === T_DO
|| $type === T_IF || $type === T_ELSEIF
|| $type === T_TRY || $type === T_CATCH || $type === T_FINALLY
) {
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true);
if ($next === false) {
break;
}
$nextType = $tokens[$next]['code'];
// Let additional conditions loop and find their ending.
if (($type === T_IF
|| $type === T_ELSEIF)
&& ($nextType === T_ELSEIF
|| $nextType === T_ELSE)
) {
continue;
}
// Account for TRY... CATCH/FINALLY statements.
if (($type === T_TRY
|| $type === T_CATCH
|| $type === T_FINALLY)
&& ($nextType === T_CATCH
|| $nextType === T_FINALLY)
) {
continue;
}
// Account for DO... WHILE conditions.
if ($type === T_DO && $nextType === T_WHILE) {
$end = $phpcsFile->findNext(T_SEMICOLON, ($next + 1));
}
} else if ($type === T_CLOSURE) {
// There should be a semicolon after the closing brace.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true);
if ($next !== false && $tokens[$next]['code'] === T_SEMICOLON) {
$end = $next;
}
}//end if
if ($tokens[$end]['code'] !== T_END_HEREDOC
&& $tokens[$end]['code'] !== T_END_NOWDOC
) {
break;
}
}//end if
if (isset($tokens[$end]['parenthesis_closer']) === true) {
$end = $tokens[$end]['parenthesis_closer'];
$lastNonEmpty = $end;
continue;
}
if ($tokens[$end]['code'] !== T_WHITESPACE) {
$lastNonEmpty = $end;
}
}//end for
if ($end === $phpcsFile->numTokens) {
$end = $lastNonEmpty;
}
$nextContent = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true);
if ($nextContent === false || $tokens[$nextContent]['line'] !== $tokens[$end]['line']) {
// Looks for completely empty statements.
$next = $phpcsFile->findNext(T_WHITESPACE, ($closer + 1), ($end + 1), true);
} else {
$next = ($end + 1);
$endLine = $end;
}
if ($next !== $end) {
if ($nextContent === false || $tokens[$nextContent]['line'] !== $tokens[$end]['line']) {
// Account for a comment on the end of the line.
for ($endLine = $end; $endLine < $phpcsFile->numTokens; $endLine++) {
if (isset($tokens[($endLine + 1)]) === false
|| $tokens[$endLine]['line'] !== $tokens[($endLine + 1)]['line']
) {
break;
}
}
if (isset(Tokens::$commentTokens[$tokens[$endLine]['code']]) === false
&& ($tokens[$endLine]['code'] !== T_WHITESPACE
|| isset(Tokens::$commentTokens[$tokens[($endLine - 1)]['code']]) === false)
) {
$endLine = $end;
}
}
if ($endLine !== $end) {
$endToken = $endLine;
$addedContent = '';
} else {
$endToken = $end;
$addedContent = $phpcsFile->eolChar;
if ($tokens[$end]['code'] !== T_SEMICOLON
&& $tokens[$end]['code'] !== T_CLOSE_CURLY_BRACKET
) {
$phpcsFile->fixer->addContent($end, '; ');
}
}
$next = $phpcsFile->findNext(T_WHITESPACE, ($endToken + 1), null, true);
if ($next !== false
&& ($tokens[$next]['code'] === T_ELSE
|| $tokens[$next]['code'] === T_ELSEIF)
) {
$phpcsFile->fixer->addContentBefore($next, '} ');
} else {
$indent = '';
for ($first = $stackPtr; $first > 0; $first--) {
if ($tokens[$first]['column'] === 1) {
break;
}
}
if ($tokens[$first]['code'] === T_WHITESPACE) {
$indent = $tokens[$first]['content'];
} else if ($tokens[$first]['code'] === T_INLINE_HTML
|| $tokens[$first]['code'] === T_OPEN_TAG
) {
$addedContent = '';
}
$addedContent .= $indent.'}';
if ($next !== false && $tokens[$endToken]['code'] === T_COMMENT) {
$addedContent .= $phpcsFile->eolChar;
}
$phpcsFile->fixer->addContent($endToken, $addedContent);
}//end if
} else {
if ($nextContent === false || $tokens[$nextContent]['line'] !== $tokens[$end]['line']) {
// Account for a comment on the end of the line.
for ($endLine = $end; $endLine < $phpcsFile->numTokens; $endLine++) {
if (isset($tokens[($endLine + 1)]) === false
|| $tokens[$endLine]['line'] !== $tokens[($endLine + 1)]['line']
) {
break;
}
}
if ($tokens[$endLine]['code'] !== T_COMMENT
&& ($tokens[$endLine]['code'] !== T_WHITESPACE
|| $tokens[($endLine - 1)]['code'] !== T_COMMENT)
) {
$endLine = $end;
}
}
if ($endLine !== $end) {
$phpcsFile->fixer->replaceToken($end, '');
$phpcsFile->fixer->addNewlineBefore($endLine);
$phpcsFile->fixer->addContent($endLine, '}');
} else {
$phpcsFile->fixer->replaceToken($end, '}');
}
}//end if
$phpcsFile->fixer->endChangeset();
}//end process()
}//end class
@@ -0,0 +1,98 @@
<?php
/**
* Runs csslint on the file.
*
* @author Roman Levishchenko <index.0h@gmail.com>
* @copyright 2013-2014 Roman Levishchenko
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*
* @deprecated 3.9.0
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Debug;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Common;
class CSSLintSniff implements Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = ['CSS'];
/**
* Returns the token types that this sniff is interested in.
*
* @return array<int|string>
*/
public function register()
{
return [T_OPEN_TAG];
}//end register()
/**
* Processes the tokens that this sniff is interested in.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where the token was found.
* @param int $stackPtr The position in the stack where
* the token was found.
*
* @return int
*/
public function process(File $phpcsFile, $stackPtr)
{
$csslintPath = Config::getExecutablePath('csslint');
if ($csslintPath === null) {
return $phpcsFile->numTokens;
}
$fileName = $phpcsFile->getFilename();
$cmd = Common::escapeshellcmd($csslintPath).' '.escapeshellarg($fileName).' 2>&1';
exec($cmd, $output, $retval);
if (is_array($output) === false) {
return $phpcsFile->numTokens;
}
$count = count($output);
for ($i = 0; $i < $count; $i++) {
$matches = [];
$numMatches = preg_match(
'/(error|warning) at line (\d+)/',
$output[$i],
$matches
);
if ($numMatches === 0) {
continue;
}
$line = (int) $matches[2];
$message = 'csslint says: '.$output[($i + 1)];
// First line is message with error line and error code.
// Second is error message.
// Third is wrong line in file.
// Fourth is empty line.
$i += 4;
$phpcsFile->addWarningOnLine($message, $line, 'ExternalTool');
}//end for
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,119 @@
<?php
/**
* Runs gjslint on the file.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*
* @deprecated 3.9.0
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Debug;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Common;
class ClosureLinterSniff implements Sniff
{
/**
* A list of error codes that should show errors.
*
* All other error codes will show warnings.
*
* @var array
*/
public $errorCodes = [];
/**
* A list of error codes to ignore.
*
* @var array
*/
public $ignoreCodes = [];
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = ['JS'];
/**
* Returns the token types that this sniff is interested in.
*
* @return array<int|string>
*/
public function register()
{
return [T_OPEN_TAG];
}//end register()
/**
* Processes the tokens that this sniff is interested in.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where the token was found.
* @param int $stackPtr The position in the stack where
* the token was found.
*
* @return int
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If jslint.js could not be run.
*/
public function process(File $phpcsFile, $stackPtr)
{
$lintPath = Config::getExecutablePath('gjslint');
if ($lintPath === null) {
return $phpcsFile->numTokens;
}
$fileName = $phpcsFile->getFilename();
$lintPath = Common::escapeshellcmd($lintPath);
$cmd = $lintPath.' --nosummary --notime --unix_mode '.escapeshellarg($fileName);
exec($cmd, $output, $retval);
if (is_array($output) === false) {
return $phpcsFile->numTokens;
}
foreach ($output as $finding) {
$matches = [];
$numMatches = preg_match('/^(.*):([0-9]+):\(.*?([0-9]+)\)(.*)$/', $finding, $matches);
if ($numMatches === 0) {
continue;
}
// Skip error codes we are ignoring.
$code = $matches[3];
if (in_array($code, $this->ignoreCodes) === true) {
continue;
}
$line = (int) $matches[2];
$error = trim($matches[4]);
$message = 'gjslint says: (%s) %s';
$data = [
$code,
$error,
];
if (in_array($code, $this->errorCodes) === true) {
$phpcsFile->addErrorOnLine($message, $line, 'ExternalToolError', $data);
} else {
$phpcsFile->addWarningOnLine($message, $line, 'ExternalTool', $data);
}
}//end foreach
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,115 @@
<?php
/**
* Runs eslint on the file.
*
* @author Ryan McCue <ryan+gh@hmn.md>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*
* @deprecated 3.9.0
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Debug;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Common;
class ESLintSniff implements Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = ['JS'];
/**
* ESLint configuration file path.
*
* @var string|null Path to eslintrc. Null to autodetect.
*/
public $configFile = null;
/**
* Returns the token types that this sniff is interested in.
*
* @return array<int|string>
*/
public function register()
{
return [T_OPEN_TAG];
}//end register()
/**
* Processes the tokens that this sniff is interested in.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where the token was found.
* @param int $stackPtr The position in the stack where
* the token was found.
*
* @return int
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If jshint.js could not be run.
*/
public function process(File $phpcsFile, $stackPtr)
{
$eslintPath = Config::getExecutablePath('eslint');
if ($eslintPath === null) {
return $phpcsFile->numTokens;
}
$filename = $phpcsFile->getFilename();
$configFile = $this->configFile;
if (empty($configFile) === true) {
// Attempt to autodetect.
$candidates = glob('.eslintrc{.js,.yaml,.yml,.json}', GLOB_BRACE);
if (empty($candidates) === false) {
$configFile = $candidates[0];
}
}
$eslintOptions = ['--format json'];
if (empty($configFile) === false) {
$eslintOptions[] = '--config '.escapeshellarg($configFile);
}
$cmd = Common::escapeshellcmd(escapeshellarg($eslintPath).' '.implode(' ', $eslintOptions).' '.escapeshellarg($filename));
// Execute!
exec($cmd, $stdout, $code);
if ($code <= 0) {
// No errors, continue.
return $phpcsFile->numTokens;
}
$data = json_decode(implode("\n", $stdout));
if (json_last_error() !== JSON_ERROR_NONE) {
// Ignore any errors.
return $phpcsFile->numTokens;
}
// Data is a list of files, but we only pass a single one.
$messages = $data[0]->messages;
foreach ($messages as $error) {
$message = 'eslint says: '.$error->message;
if (empty($error->fatal) === false || $error->severity === 2) {
$phpcsFile->addErrorOnLine($message, $error->line, 'ExternalTool');
} else {
$phpcsFile->addWarningOnLine($message, $error->line, 'ExternalTool');
}
}
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,97 @@
<?php
/**
* Runs jshint.js on the file.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Alexander Wei§ <aweisswa@gmx.de>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*
* @deprecated 3.9.0
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Debug;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Common;
class JSHintSniff implements Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = ['JS'];
/**
* Returns the token types that this sniff is interested in.
*
* @return array<int|string>
*/
public function register()
{
return [T_OPEN_TAG];
}//end register()
/**
* Processes the tokens that this sniff is interested in.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where the token was found.
* @param int $stackPtr The position in the stack where
* the token was found.
*
* @return int
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If jshint.js could not be run.
*/
public function process(File $phpcsFile, $stackPtr)
{
$rhinoPath = Config::getExecutablePath('rhino');
$jshintPath = Config::getExecutablePath('jshint');
if ($jshintPath === null) {
return $phpcsFile->numTokens;
}
$fileName = $phpcsFile->getFilename();
$jshintPath = Common::escapeshellcmd($jshintPath);
if ($rhinoPath !== null) {
$rhinoPath = Common::escapeshellcmd($rhinoPath);
$cmd = "$rhinoPath \"$jshintPath\" ".escapeshellarg($fileName);
exec($cmd, $output, $retval);
$regex = '`^(?P<error>.+)\(.+:(?P<line>[0-9]+).*:[0-9]+\)$`';
} else {
$cmd = "$jshintPath ".escapeshellarg($fileName);
exec($cmd, $output, $retval);
$regex = '`^(.+?): line (?P<line>[0-9]+), col [0-9]+, (?P<error>.+)$`';
}
if (is_array($output) === true) {
foreach ($output as $finding) {
$matches = [];
$numMatches = preg_match($regex, $finding, $matches);
if ($numMatches === 0) {
continue;
}
$line = (int) $matches['line'];
$message = 'jshint says: '.trim($matches['error']);
$phpcsFile->addWarningOnLine($message, $line, 'ExternalTool');
}
}
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,82 @@
<?php
/**
* A simple sniff for detecting a BOM definition that may corrupt application work.
*
* @author Piotr Karas <office@mediaself.pl>
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2010-2014 mediaSELF Sp. z o.o.
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Files;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class ByteOrderMarkSniff implements Sniff
{
/**
* List of supported BOM definitions.
*
* Use encoding names as keys and hex BOM representations as values.
*
* @var array
*/
protected $bomDefinitions = [
'UTF-8' => 'efbbbf',
'UTF-16 (BE)' => 'feff',
'UTF-16 (LE)' => 'fffe',
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_INLINE_HTML];
}//end register()
/**
* Processes this sniff, 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 passed in $tokens.
*
* @return int
*/
public function process(File $phpcsFile, $stackPtr)
{
// The BOM will be the very first token in the file.
if ($stackPtr !== 0) {
return $phpcsFile->numTokens;
}
$tokens = $phpcsFile->getTokens();
foreach ($this->bomDefinitions as $bomName => $expectedBomHex) {
$bomByteLength = (strlen($expectedBomHex) / 2);
$htmlBomHex = bin2hex(substr($tokens[$stackPtr]['content'], 0, $bomByteLength));
if ($htmlBomHex === $expectedBomHex) {
$errorData = [$bomName];
$error = 'File contains %s byte order mark, which may corrupt your application';
$phpcsFile->addError($error, $stackPtr, 'Found', $errorData);
$phpcsFile->recordMetric($stackPtr, 'Using byte order mark', 'yes');
return $phpcsFile->numTokens;
}
}
$phpcsFile->recordMetric($stackPtr, 'Using byte order mark', 'no');
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,84 @@
<?php
/**
* Ensures the file ends with a newline character.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Files;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class EndFileNewlineSniff implements Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = [
'PHP',
'JS',
'CSS',
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_OPEN_TAG,
T_OPEN_TAG_WITH_ECHO,
];
}//end register()
/**
* Processes this sniff, 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 passed in $tokens.
*
* @return int
*/
public function process(File $phpcsFile, $stackPtr)
{
// Skip to the end of the file.
$tokens = $phpcsFile->getTokens();
$stackPtr = ($phpcsFile->numTokens - 1);
if ($tokens[$stackPtr]['content'] === '') {
$stackPtr--;
}
$eolCharLen = strlen($phpcsFile->eolChar);
$lastChars = substr($tokens[$stackPtr]['content'], ($eolCharLen * -1));
if ($lastChars !== $phpcsFile->eolChar) {
$phpcsFile->recordMetric($stackPtr, 'Newline at EOF', 'no');
$error = 'File must end with a newline character';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'NotFound');
if ($fix === true) {
$phpcsFile->fixer->addNewline($stackPtr);
}
} else {
$phpcsFile->recordMetric($stackPtr, 'Newline at EOF', 'yes');
}
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,91 @@
<?php
/**
* Ensures the file does not end with a newline character.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Files;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class EndFileNoNewlineSniff implements Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = [
'PHP',
'JS',
'CSS',
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_OPEN_TAG,
T_OPEN_TAG_WITH_ECHO,
];
}//end register()
/**
* Processes this sniff, 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 passed in $tokens.
*
* @return int
*/
public function process(File $phpcsFile, $stackPtr)
{
// Skip to the end of the file.
$tokens = $phpcsFile->getTokens();
$stackPtr = ($phpcsFile->numTokens - 1);
if ($tokens[$stackPtr]['content'] === '') {
--$stackPtr;
}
$eolCharLen = strlen($phpcsFile->eolChar);
$lastChars = substr($tokens[$stackPtr]['content'], ($eolCharLen * -1));
if ($lastChars === $phpcsFile->eolChar) {
$error = 'File must not end with a newline character';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'Found');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
for ($i = $stackPtr; $i > 0; $i--) {
$newContent = rtrim($tokens[$i]['content'], $phpcsFile->eolChar);
$phpcsFile->fixer->replaceToken($i, $newContent);
if ($newContent !== '') {
break;
}
}
$phpcsFile->fixer->endChangeset();
}
}
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,62 @@
<?php
/**
* Tests that files are not executable.
*
* @author Matthew Peveler <matt.peveler@gmail.com>
* @copyright 2019 Matthew Peveler
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Files;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class ExecutableFileSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_OPEN_TAG,
T_OPEN_TAG_WITH_ECHO,
];
}//end register()
/**
* 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 passed in $tokens.
*
* @return int
*/
public function process(File $phpcsFile, $stackPtr)
{
$filename = $phpcsFile->getFilename();
if ($filename !== 'STDIN') {
$perms = fileperms($phpcsFile->getFilename());
if (($perms & 0x0040) !== 0 || ($perms & 0x0008) !== 0 || ($perms & 0x0001) !== 0) {
$error = 'A PHP file should not be executable; found file permissions set to %s';
$data = [substr(sprintf('%o', $perms), -4)];
$phpcsFile->addError($error, 0, 'Executable', $data);
}
}
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,79 @@
<?php
/**
* Ensures the whole file is PHP only, with no whitespace or inline HTML.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Files;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class InlineHTMLSniff implements Sniff
{
/**
* List of supported BOM definitions.
*
* Use encoding names as keys and hex BOM representations as values.
*
* @var array
*/
protected $bomDefinitions = [
'UTF-8' => 'efbbbf',
'UTF-16 (BE)' => 'feff',
'UTF-16 (LE)' => 'fffe',
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_INLINE_HTML];
}//end register()
/**
* 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 passed in $tokens.
*
* @return int|void
*/
public function process(File $phpcsFile, $stackPtr)
{
// Allow a byte-order mark.
$tokens = $phpcsFile->getTokens();
foreach ($this->bomDefinitions as $expectedBomHex) {
$bomByteLength = (strlen($expectedBomHex) / 2);
$htmlBomHex = bin2hex(substr($tokens[0]['content'], 0, $bomByteLength));
if ($htmlBomHex === $expectedBomHex && strlen($tokens[0]['content']) === $bomByteLength) {
return;
}
}
// Ignore shebang lines.
$tokens = $phpcsFile->getTokens();
if (substr($tokens[$stackPtr]['content'], 0, 2) === '#!') {
return;
}
$error = 'PHP files must only contain PHP code';
$phpcsFile->addError($error, $stackPtr, 'Found');
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,148 @@
<?php
/**
* Checks that end of line characters are correct.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Files;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class LineEndingsSniff implements Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = [
'PHP',
'JS',
'CSS',
];
/**
* The valid EOL character.
*
* @var string
*/
public $eolChar = '\n';
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_OPEN_TAG,
T_OPEN_TAG_WITH_ECHO,
];
}//end register()
/**
* Processes this sniff, 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 passed in $tokens.
*
* @return int
*/
public function process(File $phpcsFile, $stackPtr)
{
$found = $phpcsFile->eolChar;
$found = str_replace("\n", '\n', $found);
$found = str_replace("\r", '\r', $found);
$phpcsFile->recordMetric($stackPtr, 'EOL char', $found);
if ($found === $this->eolChar) {
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}
// Check for single line files without an EOL. This is a very special
// case and the EOL char is set to \n when this happens.
if ($found === '\n') {
$tokens = $phpcsFile->getTokens();
$lastToken = ($phpcsFile->numTokens - 1);
if ($tokens[$lastToken]['line'] === 1
&& $tokens[$lastToken]['content'] !== "\n"
) {
return $phpcsFile->numTokens;
}
}
$error = 'End of line character is invalid; expected "%s" but found "%s"';
$expected = $this->eolChar;
$expected = str_replace("\n", '\n', $expected);
$expected = str_replace("\r", '\r', $expected);
$data = [
$expected,
$found,
];
// Errors are always reported on line 1, no matter where the first PHP tag is.
$fix = $phpcsFile->addFixableError($error, 0, 'InvalidEOLChar', $data);
if ($fix === true) {
$tokens = $phpcsFile->getTokens();
switch ($this->eolChar) {
case '\n':
$eolChar = "\n";
break;
case '\r':
$eolChar = "\r";
break;
case '\r\n':
$eolChar = "\r\n";
break;
default:
$eolChar = $this->eolChar;
break;
}
for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
if (isset($tokens[($i + 1)]) === true
&& $tokens[($i + 1)]['line'] <= $tokens[$i]['line']
) {
continue;
}
// Token is the last on a line.
if (isset($tokens[$i]['orig_content']) === true) {
$tokenContent = $tokens[$i]['orig_content'];
} else {
$tokenContent = $tokens[$i]['content'];
}
if ($tokenContent === '') {
// Special case for JS/CSS close tag.
continue;
}
$newContent = rtrim($tokenContent, "\r\n");
$newContent .= $eolChar;
if ($tokenContent !== $newContent) {
$phpcsFile->fixer->replaceToken($i, $newContent);
}
}//end for
}//end if
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,201 @@
<?php
/**
* Checks the length of all lines in a file.
*
* Checks all lines in the file, and throws warnings if they are over 80
* characters in length and errors if they are over 100. Both these
* figures can be changed in a ruleset.xml file.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Files;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class LineLengthSniff implements Sniff
{
/**
* The limit that the length of a line should not exceed.
*
* @var integer
*/
public $lineLimit = 80;
/**
* The limit that the length of a line must not exceed.
*
* Set to zero (0) to disable.
*
* @var integer
*/
public $absoluteLineLimit = 100;
/**
* Whether or not to ignore trailing comments.
*
* This has the effect of also ignoring all lines
* that only contain comments.
*
* @var boolean
*/
public $ignoreComments = false;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_OPEN_TAG];
}//end register()
/**
* 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 passed in $tokens.
*
* @return int
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
for ($i = 1; $i < $phpcsFile->numTokens; $i++) {
if ($tokens[$i]['column'] === 1) {
$this->checkLineLength($phpcsFile, $tokens, $i);
}
}
$this->checkLineLength($phpcsFile, $tokens, $i);
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
/**
* Checks if a line is too long.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param array $tokens The token stack.
* @param int $stackPtr The first token on the next line.
*
* @return void
*/
protected function checkLineLength($phpcsFile, $tokens, $stackPtr)
{
// The passed token is the first on the line.
$stackPtr--;
if ($tokens[$stackPtr]['column'] === 1
&& $tokens[$stackPtr]['length'] === 0
) {
// Blank line.
return;
}
if ($tokens[$stackPtr]['column'] !== 1
&& $tokens[$stackPtr]['content'] === $phpcsFile->eolChar
) {
$stackPtr--;
}
$onlyComment = false;
if (isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true) {
$prevNonWhiteSpace = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($tokens[$stackPtr]['line'] !== $tokens[$prevNonWhiteSpace]['line']) {
$onlyComment = true;
}
}
if ($onlyComment === true
&& isset(Tokens::$phpcsCommentTokens[$tokens[$stackPtr]['code']]) === true
) {
// Ignore PHPCS annotation comments that are on a line by themselves.
return;
}
$lineLength = ($tokens[$stackPtr]['column'] + $tokens[$stackPtr]['length'] - 1);
if ($this->ignoreComments === true
&& isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true
) {
// Trailing comments are being ignored in line length calculations.
if ($onlyComment === true) {
// The comment is the only thing on the line, so no need to check length.
return;
}
$lineLength -= $tokens[$stackPtr]['length'];
}
// Record metrics for common line length groupings.
if ($lineLength <= 80) {
$phpcsFile->recordMetric($stackPtr, 'Line length', '80 or less');
} else if ($lineLength <= 120) {
$phpcsFile->recordMetric($stackPtr, 'Line length', '81-120');
} else if ($lineLength <= 150) {
$phpcsFile->recordMetric($stackPtr, 'Line length', '121-150');
} else {
$phpcsFile->recordMetric($stackPtr, 'Line length', '151 or more');
}
if ($onlyComment === true) {
// If this is a long comment, check if it can be broken up onto multiple lines.
// Some comments contain unbreakable strings like URLs and so it makes sense
// to ignore the line length in these cases if the URL would be longer than the max
// line length once you indent it to the correct level.
if ($lineLength > $this->lineLimit) {
$oldLength = strlen($tokens[$stackPtr]['content']);
$newLength = strlen(ltrim($tokens[$stackPtr]['content'], "/#\t "));
$indent = (($tokens[$stackPtr]['column'] - 1) + ($oldLength - $newLength));
$nonBreakingLength = $tokens[$stackPtr]['length'];
$space = strrpos($tokens[$stackPtr]['content'], ' ');
if ($space !== false) {
$nonBreakingLength -= ($space + 1);
}
if (($nonBreakingLength + $indent) > $this->lineLimit) {
return;
}
}
}//end if
if ($this->absoluteLineLimit > 0
&& $lineLength > $this->absoluteLineLimit
) {
$data = [
$this->absoluteLineLimit,
$lineLength,
];
$error = 'Line exceeds maximum limit of %s characters; contains %s characters';
$phpcsFile->addError($error, $stackPtr, 'MaxExceeded', $data);
} else if ($lineLength > $this->lineLimit) {
$data = [
$this->lineLimit,
$lineLength,
];
$warning = 'Line exceeds %s characters; contains %s characters';
$phpcsFile->addWarning($warning, $stackPtr, 'TooLong', $data);
}
}//end checkLineLength()
}//end class
@@ -0,0 +1,70 @@
<?php
/**
* Checks that all file names are lowercased.
*
* @author Andy Grunwald <andygrunwald@gmail.com>
* @copyright 2010-2014 Andy Grunwald
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Files;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class LowercasedFilenameSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_OPEN_TAG,
T_OPEN_TAG_WITH_ECHO,
];
}//end register()
/**
* Processes this sniff, 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 passed in $tokens.
*
* @return int
*/
public function process(File $phpcsFile, $stackPtr)
{
$filename = $phpcsFile->getFilename();
if ($filename === 'STDIN') {
return $phpcsFile->numTokens;
}
$filename = basename($filename);
$lowercaseFilename = strtolower($filename);
if ($filename !== $lowercaseFilename) {
$data = [
$filename,
$lowercaseFilename,
];
$error = 'Filename "%s" doesn\'t match the expected filename "%s"';
$phpcsFile->addError($error, $stackPtr, 'NotFound', $data);
$phpcsFile->recordMetric($stackPtr, 'Lowercase filename', 'no');
} else {
$phpcsFile->recordMetric($stackPtr, 'Lowercase filename', 'yes');
}
// Ignore the rest of the file.
return $phpcsFile->numTokens;
}//end process()
}//end class
@@ -0,0 +1,141 @@
<?php
/**
* Ensures that variables are not passed by reference when calling a function.
*
* @author Florian Grandel <jerico.dev@gmail.com>
* @copyright 2009-2014 Florian Grandel
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Functions;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class CallTimePassByReferenceSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_STRING,
T_VARIABLE,
T_ANON_CLASS,
T_PARENT,
T_SELF,
T_STATIC,
];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$findTokens = Tokens::$emptyTokens;
$findTokens[] = T_BITWISE_AND;
$prev = $phpcsFile->findPrevious($findTokens, ($stackPtr - 1), null, true);
// Skip tokens that are the names of functions
// within their definitions. For example: function myFunction...
// "myFunction" is T_STRING but we should skip because it is not a
// function or method *call*.
$prevCode = $tokens[$prev]['code'];
if ($prevCode === T_FUNCTION) {
return;
}
// If the next non-whitespace token after the function or method call
// is not an opening parenthesis then it cant really be a *call*.
$functionName = $stackPtr;
$openBracket = $phpcsFile->findNext(
Tokens::$emptyTokens,
($functionName + 1),
null,
true
);
if ($openBracket === false || $tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) {
return;
}
if (isset($tokens[$openBracket]['parenthesis_closer']) === false) {
return;
}
$closeBracket = $tokens[$openBracket]['parenthesis_closer'];
$nextSeparator = $openBracket;
$find = [
T_VARIABLE,
T_OPEN_SHORT_ARRAY,
];
while (($nextSeparator = $phpcsFile->findNext($find, ($nextSeparator + 1), $closeBracket)) !== false) {
if ($tokens[$nextSeparator]['code'] === T_OPEN_SHORT_ARRAY) {
$nextSeparator = $tokens[$nextSeparator]['bracket_closer'];
continue;
}
// Make sure the variable belongs directly to this function call
// and is not inside a nested function call or array.
$brackets = $tokens[$nextSeparator]['nested_parenthesis'];
$lastBracket = array_pop($brackets);
if ($lastBracket !== $closeBracket) {
continue;
}
$tokenBefore = $phpcsFile->findPrevious(
Tokens::$emptyTokens,
($nextSeparator - 1),
null,
true
);
if ($tokens[$tokenBefore]['code'] === T_BITWISE_AND) {
if ($phpcsFile->isReference($tokenBefore) === false) {
continue;
}
// We also want to ignore references used in assignment
// operations passed as function arguments, but isReference()
// sees them as valid references (which they are).
$tokenBefore = $phpcsFile->findPrevious(
Tokens::$emptyTokens,
($tokenBefore - 1),
null,
true
);
if (isset(Tokens::$assignmentTokens[$tokens[$tokenBefore]['code']]) === true) {
continue;
}
// T_BITWISE_AND represents a pass-by-reference.
$error = 'Call-time pass-by-reference calls are prohibited';
$phpcsFile->addError($error, $tokenBefore, 'NotAllowed');
}//end if
}//end while
}//end process()
}//end class
@@ -0,0 +1,197 @@
<?php
/**
* Checks that calls to methods and functions are spaced correctly.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Functions;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class FunctionCallArgumentSpacingSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return[
T_STRING,
T_ISSET,
T_UNSET,
T_SELF,
T_STATIC,
T_PARENT,
T_VARIABLE,
T_CLOSE_CURLY_BRACKET,
T_CLOSE_PARENTHESIS,
];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// Skip tokens that are the names of functions or classes
// within their definitions. For example:
// function myFunction...
// "myFunction" is T_STRING but we should skip because it is not a
// function or method *call*.
$functionName = $stackPtr;
$ignoreTokens = Tokens::$emptyTokens;
$ignoreTokens[] = T_BITWISE_AND;
$functionKeyword = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true);
if ($tokens[$functionKeyword]['code'] === T_FUNCTION || $tokens[$functionKeyword]['code'] === T_CLASS) {
return;
}
if ($tokens[$stackPtr]['code'] === T_CLOSE_CURLY_BRACKET
&& isset($tokens[$stackPtr]['scope_condition']) === true
) {
// Not a function call.
return;
}
// If the next non-whitespace token after the function or method call
// is not an opening parenthesis then it can't really be a *call*.
$openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($functionName + 1), null, true);
if ($tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) {
return;
}
if (isset($tokens[$openBracket]['parenthesis_closer']) === false) {
return;
}
$this->checkSpacing($phpcsFile, $stackPtr, $openBracket);
}//end process()
/**
* Checks the spacing around commas.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
* @param int $openBracket The position of the opening bracket
* in the stack passed in $tokens.
*
* @return void
*/
public function checkSpacing(File $phpcsFile, $stackPtr, $openBracket)
{
$tokens = $phpcsFile->getTokens();
$closeBracket = $tokens[$openBracket]['parenthesis_closer'];
$nextSeparator = $openBracket;
$find = [
T_COMMA,
T_CLOSURE,
T_FN,
T_ANON_CLASS,
T_OPEN_SHORT_ARRAY,
T_MATCH,
];
while (($nextSeparator = $phpcsFile->findNext($find, ($nextSeparator + 1), $closeBracket)) !== false) {
if ($tokens[$nextSeparator]['code'] === T_CLOSURE
|| $tokens[$nextSeparator]['code'] === T_ANON_CLASS
|| $tokens[$nextSeparator]['code'] === T_MATCH
) {
// Skip closures, anon class declarations and match control structures.
$nextSeparator = $tokens[$nextSeparator]['scope_closer'];
continue;
} else if ($tokens[$nextSeparator]['code'] === T_FN) {
// Skip arrow functions, but don't skip the arrow function closer as it is likely to
// be the comma separating it from the next function call argument (or the parenthesis closer).
$nextSeparator = ($tokens[$nextSeparator]['scope_closer'] - 1);
continue;
} else if ($tokens[$nextSeparator]['code'] === T_OPEN_SHORT_ARRAY) {
// Skips arrays using short notation.
$nextSeparator = $tokens[$nextSeparator]['bracket_closer'];
continue;
}
// Make sure the comma or variable belongs directly to this function call,
// and is not inside a nested function call or array.
$brackets = $tokens[$nextSeparator]['nested_parenthesis'];
$lastBracket = array_pop($brackets);
if ($lastBracket !== $closeBracket) {
continue;
}
if ($tokens[$nextSeparator]['code'] === T_COMMA) {
if ($tokens[($nextSeparator - 1)]['code'] === T_WHITESPACE) {
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($nextSeparator - 2), null, true);
if (isset(Tokens::$heredocTokens[$tokens[$prev]['code']]) === false) {
$error = 'Space found before comma in argument list';
$fix = $phpcsFile->addFixableError($error, $nextSeparator, 'SpaceBeforeComma');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
if ($tokens[$prev]['line'] !== $tokens[$nextSeparator]['line']) {
$phpcsFile->fixer->addContent($prev, ',');
$phpcsFile->fixer->replaceToken($nextSeparator, '');
} else {
$phpcsFile->fixer->replaceToken(($nextSeparator - 1), '');
}
$phpcsFile->fixer->endChangeset();
}
}//end if
}//end if
if ($tokens[($nextSeparator + 1)]['code'] !== T_WHITESPACE) {
// Ignore trailing comma's after last argument as that's outside the scope of this sniff.
if (($nextSeparator + 1) !== $closeBracket) {
$error = 'No space found after comma in argument list';
$fix = $phpcsFile->addFixableError($error, $nextSeparator, 'NoSpaceAfterComma');
if ($fix === true) {
$phpcsFile->fixer->addContent($nextSeparator, ' ');
}
}
} else {
// If there is a newline in the space, then they must be formatting
// each argument on a newline, which is valid, so ignore it.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextSeparator + 1), null, true);
if ($tokens[$next]['line'] === $tokens[$nextSeparator]['line']) {
$space = $tokens[($nextSeparator + 1)]['length'];
if ($space > 1) {
$error = 'Expected 1 space after comma in argument list; %s found';
$data = [$space];
$fix = $phpcsFile->addFixableError($error, $nextSeparator, 'TooMuchSpaceAfterComma', $data);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($nextSeparator + 1), ' ');
}
}
}
}//end if
}//end if
}//end while
}//end checkSpacing()
}//end class
@@ -0,0 +1,177 @@
<?php
/**
* Checks that the opening brace of a function is on the same line as the function declaration.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Functions;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class OpeningFunctionBraceKernighanRitchieSniff implements Sniff
{
/**
* Should this sniff check function braces?
*
* @var boolean
*/
public $checkFunctions = true;
/**
* Should this sniff check closure braces?
*
* @var boolean
*/
public $checkClosures = false;
/**
* Registers the tokens that this sniff wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_FUNCTION,
T_CLOSURE,
];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['scope_opener']) === false) {
return;
}
if (($tokens[$stackPtr]['code'] === T_FUNCTION
&& (bool) $this->checkFunctions === false)
|| ($tokens[$stackPtr]['code'] === T_CLOSURE
&& (bool) $this->checkClosures === false)
) {
return;
}
$openingBrace = $tokens[$stackPtr]['scope_opener'];
// Find the end of the function declaration.
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($openingBrace - 1), null, true);
$functionLine = $tokens[$prev]['line'];
$braceLine = $tokens[$openingBrace]['line'];
$lineDifference = ($braceLine - $functionLine);
$metricType = 'Function';
if ($tokens[$stackPtr]['code'] === T_CLOSURE) {
$metricType = 'Closure';
}
if ($lineDifference > 0) {
$phpcsFile->recordMetric($stackPtr, "$metricType opening brace placement", 'new line');
$error = 'Opening brace should be on the same line as the declaration';
$fix = $phpcsFile->addFixableError($error, $openingBrace, 'BraceOnNewLine');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->addContent($prev, ' {');
$phpcsFile->fixer->replaceToken($openingBrace, '');
if ($tokens[($openingBrace + 1)]['code'] === T_WHITESPACE
&& $tokens[($openingBrace + 2)]['line'] > $tokens[$openingBrace]['line']
) {
// Brace is followed by a new line, so remove it to ensure we don't
// leave behind a blank line at the top of the block.
$phpcsFile->fixer->replaceToken(($openingBrace + 1), '');
if ($tokens[($openingBrace - 1)]['code'] === T_WHITESPACE
&& $tokens[($openingBrace - 1)]['line'] === $tokens[$openingBrace]['line']
&& $tokens[($openingBrace - 2)]['line'] < $tokens[$openingBrace]['line']
) {
// Brace is preceded by indent, so remove it to ensure we don't
// leave behind more indent than is required for the first line.
$phpcsFile->fixer->replaceToken(($openingBrace - 1), '');
}
}
$phpcsFile->fixer->endChangeset();
}//end if
} else {
$phpcsFile->recordMetric($stackPtr, "$metricType opening brace placement", 'same line');
}//end if
$ignore = Tokens::$phpcsCommentTokens;
$ignore[] = T_WHITESPACE;
$next = $phpcsFile->findNext($ignore, ($openingBrace + 1), null, true);
if ($tokens[$next]['line'] === $tokens[$openingBrace]['line']) {
// Only throw this error when this is not an empty function.
if ($next !== $tokens[$stackPtr]['scope_closer']
&& $tokens[$next]['code'] !== T_CLOSE_TAG
) {
$error = 'Opening brace must be the last content on the line';
$fix = $phpcsFile->addFixableError($error, $openingBrace, 'ContentAfterBrace');
if ($fix === true) {
$phpcsFile->fixer->addNewline($openingBrace);
}
}
}
// Only continue checking if the opening brace looks good.
if ($lineDifference > 0) {
return;
}
// Enforce a single space. Tabs not allowed.
$spacing = $tokens[($openingBrace - 1)]['content'];
if ($tokens[($openingBrace - 1)]['code'] !== T_WHITESPACE) {
$length = 0;
} else if ($spacing === "\t") {
// Tab without tab-width set, so no tab replacement has taken place.
$length = '\t';
} else {
$length = strlen($spacing);
}
// If tab replacement is on, avoid confusing the user with a "expected 1 space, found 1"
// message when the "1" found is actually a tab, not a space.
if ($length === 1
&& isset($tokens[($openingBrace - 1)]['orig_content']) === true
&& $tokens[($openingBrace - 1)]['orig_content'] === "\t"
) {
$length = '\t';
}
if ($length !== 1) {
$error = 'Expected 1 space before opening brace; found %s';
$data = [$length];
$fix = $phpcsFile->addFixableError($error, $openingBrace, 'SpaceBeforeBrace', $data);
if ($fix === true) {
if ($length === 0) {
$phpcsFile->fixer->addContentBefore($openingBrace, ' ');
} else {
$phpcsFile->fixer->replaceToken(($openingBrace - 1), ' ');
}
}
}
}//end process()
}//end class
@@ -0,0 +1,117 @@
<?php
/**
* Checks the cyclomatic complexity (McCabe) for functions.
*
* The cyclomatic complexity (also called McCabe code metrics)
* indicates the complexity within a function by counting
* the different paths the function includes.
*
* @author Johann-Peter Hartmann <hartmann@mayflower.de>
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2007-2014 Mayflower GmbH
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class CyclomaticComplexitySniff implements Sniff
{
/**
* A complexity higher than this value will throw a warning.
*
* @var integer
*/
public $complexity = 10;
/**
* A complexity higher than this value will throw an error.
*
* @var integer
*/
public $absoluteComplexity = 20;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_FUNCTION];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// Ignore abstract and interface methods. Bail early when live coding.
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
return;
}
// Detect start and end of this function definition.
$start = $tokens[$stackPtr]['scope_opener'];
$end = $tokens[$stackPtr]['scope_closer'];
// Predicate nodes for PHP.
$find = [
T_CASE => true,
T_DEFAULT => true,
T_CATCH => true,
T_IF => true,
T_FOR => true,
T_FOREACH => true,
T_WHILE => true,
T_ELSEIF => true,
T_INLINE_THEN => true,
T_COALESCE => true,
T_COALESCE_EQUAL => true,
T_MATCH_ARROW => true,
T_NULLSAFE_OBJECT_OPERATOR => true,
];
$complexity = 1;
// Iterate from start to end and count predicate nodes.
for ($i = ($start + 1); $i < $end; $i++) {
if (isset($find[$tokens[$i]['code']]) === true) {
$complexity++;
}
}
if ($complexity > $this->absoluteComplexity) {
$error = 'Function\'s cyclomatic complexity (%s) exceeds allowed maximum of %s';
$data = [
$complexity,
$this->absoluteComplexity,
];
$phpcsFile->addError($error, $stackPtr, 'MaxExceeded', $data);
} else if ($complexity > $this->complexity) {
$warning = 'Function\'s cyclomatic complexity (%s) exceeds %s; consider refactoring the function';
$data = [
$complexity,
$this->complexity,
];
$phpcsFile->addWarning($warning, $stackPtr, 'TooHigh', $data);
}
}//end process()
}//end class
@@ -0,0 +1,100 @@
<?php
/**
* Checks the nesting level for methods.
*
* @author Johann-Peter Hartmann <hartmann@mayflower.de>
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2007-2014 Mayflower GmbH
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class NestingLevelSniff implements Sniff
{
/**
* A nesting level higher than this value will throw a warning.
*
* @var integer
*/
public $nestingLevel = 5;
/**
* A nesting level higher than this value will throw an error.
*
* @var integer
*/
public $absoluteNestingLevel = 10;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_FUNCTION];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// Ignore abstract and interface methods. Bail early when live coding.
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
return;
}
// Detect start and end of this function definition.
$start = $tokens[$stackPtr]['scope_opener'];
$end = $tokens[$stackPtr]['scope_closer'];
$nestingLevel = 0;
// Find the maximum nesting level of any token in the function.
for ($i = ($start + 1); $i < $end; $i++) {
$level = $tokens[$i]['level'];
if ($nestingLevel < $level) {
$nestingLevel = $level;
}
}
// We subtract the nesting level of the function itself.
$nestingLevel = ($nestingLevel - $tokens[$stackPtr]['level'] - 1);
if ($nestingLevel > $this->absoluteNestingLevel) {
$error = 'Function\'s nesting level (%s) exceeds allowed maximum of %s';
$data = [
$nestingLevel,
$this->absoluteNestingLevel,
];
$phpcsFile->addError($error, $stackPtr, 'MaxExceeded', $data);
} else if ($nestingLevel > $this->nestingLevel) {
$warning = 'Function\'s nesting level (%s) exceeds %s; consider refactoring the function';
$data = [
$nestingLevel,
$this->nestingLevel,
];
$phpcsFile->addWarning($warning, $stackPtr, 'TooHigh', $data);
}
}//end process()
}//end class
@@ -0,0 +1,60 @@
<?php
/**
* Checks that abstract classes are prefixed by Abstract.
*
* @author Anna Borzenko <annnechko@gmail.com>
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class AbstractClassNamePrefixSniff implements Sniff
{
/**
* Registers the tokens that this sniff wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_CLASS];
}//end register()
/**
* Processes this sniff, 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
if ($phpcsFile->getClassProperties($stackPtr)['is_abstract'] === false) {
// This class is not abstract so we don't need to check it.
return;
}
$className = $phpcsFile->getDeclarationName($stackPtr);
if ($className === null) {
// Live coding or parse error.
return;
}
$prefix = substr($className, 0, 8);
if (strtolower($prefix) !== 'abstract') {
$phpcsFile->addError('Abstract class names must be prefixed with "Abstract"; found "%s"', $stackPtr, 'Missing', [$className]);
}
}//end process()
}//end class
@@ -0,0 +1,222 @@
<?php
/**
* Ensures method and functions are named correctly.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\AbstractScopeSniff;
use PHP_CodeSniffer\Util\Common;
use PHP_CodeSniffer\Util\Tokens;
class CamelCapsFunctionNameSniff extends AbstractScopeSniff
{
/**
* A list of all PHP magic methods.
*
* @var array
*/
protected $magicMethods = [
'construct' => true,
'destruct' => true,
'call' => true,
'callstatic' => true,
'get' => true,
'set' => true,
'isset' => true,
'unset' => true,
'sleep' => true,
'wakeup' => true,
'serialize' => true,
'unserialize' => true,
'tostring' => true,
'invoke' => true,
'set_state' => true,
'clone' => true,
'debuginfo' => true,
];
/**
* A list of all PHP non-magic methods starting with a double underscore.
*
* These come from PHP modules such as SOAPClient.
*
* @var array
*/
protected $methodsDoubleUnderscore = [
'dorequest' => true,
'getcookies' => true,
'getfunctions' => true,
'getlastrequest' => true,
'getlastrequestheaders' => true,
'getlastresponse' => true,
'getlastresponseheaders' => true,
'gettypes' => true,
'setcookie' => true,
'setlocation' => true,
'setsoapheaders' => true,
'soapcall' => true,
];
/**
* A list of all PHP magic functions.
*
* @var array
*/
protected $magicFunctions = ['autoload' => true];
/**
* If TRUE, the string must not have two capital letters next to each other.
*
* @var boolean
*/
public $strict = true;
/**
* Constructs a Generic_Sniffs_NamingConventions_CamelCapsFunctionNameSniff.
*/
public function __construct()
{
parent::__construct(Tokens::$ooScopeTokens, [T_FUNCTION], true);
}//end __construct()
/**
* Processes the tokens within the scope.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed.
* @param int $stackPtr The position where this token was
* found.
* @param int $currScope The position of the current scope.
*
* @return void
*/
protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope)
{
$tokens = $phpcsFile->getTokens();
// Determine if this is a function which needs to be examined.
$conditions = $tokens[$stackPtr]['conditions'];
end($conditions);
$deepestScope = key($conditions);
if ($deepestScope !== $currScope) {
return;
}
$methodName = $phpcsFile->getDeclarationName($stackPtr);
if ($methodName === null) {
// Live coding or parse error. Bow out.
return;
}
$className = $phpcsFile->getDeclarationName($currScope);
if (isset($className) === false) {
$className = '[Anonymous Class]';
}
$errorData = [$className.'::'.$methodName];
$methodNameLc = strtolower($methodName);
$classNameLc = strtolower($className);
// Is this a magic method. i.e., is prefixed with "__" ?
if (preg_match('|^__[^_]|', $methodName) !== 0) {
$magicPart = substr($methodNameLc, 2);
if (isset($this->magicMethods[$magicPart]) === true
|| isset($this->methodsDoubleUnderscore[$magicPart]) === true
) {
return;
}
$error = 'Method name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore';
$phpcsFile->addError($error, $stackPtr, 'MethodDoubleUnderscore', $errorData);
}
// PHP4 constructors are allowed to break our rules.
if ($methodNameLc === $classNameLc) {
return;
}
// PHP4 destructors are allowed to break our rules.
if ($methodNameLc === '_'.$classNameLc) {
return;
}
// Ignore leading underscores in the method name.
$methodName = ltrim($methodName, '_');
$methodProps = $phpcsFile->getMethodProperties($stackPtr);
if (Common::isCamelCaps($methodName, false, true, $this->strict) === false) {
if ($methodProps['scope_specified'] === true) {
$error = '%s method name "%s" is not in camel caps format';
$data = [
ucfirst($methodProps['scope']),
$errorData[0],
];
$phpcsFile->addError($error, $stackPtr, 'ScopeNotCamelCaps', $data);
} else {
$error = 'Method name "%s" is not in camel caps format';
$phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $errorData);
}
$phpcsFile->recordMetric($stackPtr, 'CamelCase method name', 'no');
} else {
$phpcsFile->recordMetric($stackPtr, 'CamelCase method name', 'yes');
}
}//end processTokenWithinScope()
/**
* Processes the tokens outside the scope.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed.
* @param int $stackPtr The position where this token was
* found.
*
* @return void
*/
protected function processTokenOutsideScope(File $phpcsFile, $stackPtr)
{
$functionName = $phpcsFile->getDeclarationName($stackPtr);
if ($functionName === null) {
// Live coding or parse error. Bow out.
return;
}
$errorData = [$functionName];
// Is this a magic function. i.e., it is prefixed with "__".
if (preg_match('|^__[^_]|', $functionName) !== 0) {
$magicPart = strtolower(substr($functionName, 2));
if (isset($this->magicFunctions[$magicPart]) === true) {
return;
}
$error = 'Function name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore';
$phpcsFile->addError($error, $stackPtr, 'FunctionDoubleUnderscore', $errorData);
}
// Ignore leading underscores in the method name.
$functionName = ltrim($functionName, '_');
if (Common::isCamelCaps($functionName, false, true, $this->strict) === false) {
$error = 'Function name "%s" is not in camel caps format';
$phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $errorData);
$phpcsFile->recordMetric($stackPtr, 'CamelCase function name', 'no');
} else {
$phpcsFile->recordMetric($stackPtr, 'CamelCase method name', 'yes');
}
}//end processTokenOutsideScope()
}//end class
@@ -0,0 +1,178 @@
<?php
/**
* Bans PHP 4 style constructors.
*
* Favour PHP 5 constructor syntax, which uses "function __construct()".
* Avoid PHP 4 constructor syntax, which uses "function ClassName()".
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Leif Wickland <lwickland@rightnow.com>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\AbstractScopeSniff;
use PHP_CodeSniffer\Util\Tokens;
class ConstructorNameSniff extends AbstractScopeSniff
{
/**
* The name of the class we are currently checking.
*
* @var string
*/
private $currentClass = '';
/**
* A list of functions in the current class.
*
* @var string[]
*/
private $functionList = [];
/**
* Constructs the test with the tokens it wishes to listen for.
*/
public function __construct()
{
parent::__construct([T_CLASS, T_ANON_CLASS], [T_FUNCTION], true);
}//end __construct()
/**
* Processes this test when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param int $currScope A pointer to the start of the scope.
*
* @return void
*/
protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope)
{
$tokens = $phpcsFile->getTokens();
// Determine if this is a function which needs to be examined.
$conditions = $tokens[$stackPtr]['conditions'];
end($conditions);
$deepestScope = key($conditions);
if ($deepestScope !== $currScope) {
return;
}
$className = $phpcsFile->getDeclarationName($currScope);
if (empty($className) === false) {
// Not an anonymous class.
$className = strtolower($className);
}
if ($className !== $this->currentClass) {
$this->loadFunctionNamesInScope($phpcsFile, $currScope);
$this->currentClass = $className;
}
$methodName = strtolower($phpcsFile->getDeclarationName($stackPtr));
if ($methodName === $className) {
if (in_array('__construct', $this->functionList, true) === false) {
$error = 'PHP4 style constructors are not allowed; use "__construct()" instead';
$phpcsFile->addError($error, $stackPtr, 'OldStyle');
}
} else if ($methodName !== '__construct') {
// Not a constructor.
return;
}
// Stop if the constructor doesn't have a body, like when it is abstract.
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
return;
}
$parentClassName = $phpcsFile->findExtendedClassName($currScope);
if ($parentClassName === false) {
return;
}
$parentClassNameLc = strtolower($parentClassName);
$endFunctionIndex = $tokens[$stackPtr]['scope_closer'];
$startIndex = $tokens[$stackPtr]['scope_opener'];
while (($doubleColonIndex = $phpcsFile->findNext(T_DOUBLE_COLON, ($startIndex + 1), $endFunctionIndex)) !== false) {
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($doubleColonIndex + 1), null, true);
if ($tokens[$nextNonEmpty]['code'] !== T_STRING
|| strtolower($tokens[$nextNonEmpty]['content']) !== $parentClassNameLc
) {
$startIndex = $nextNonEmpty;
continue;
}
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($doubleColonIndex - 1), null, true);
if ($tokens[$prevNonEmpty]['code'] === T_PARENT
|| $tokens[$prevNonEmpty]['code'] === T_SELF
|| $tokens[$prevNonEmpty]['code'] === T_STATIC
|| ($tokens[$prevNonEmpty]['code'] === T_STRING
&& strtolower($tokens[$prevNonEmpty]['content']) === $parentClassNameLc)
) {
$error = 'PHP4 style calls to parent constructors are not allowed; use "parent::__construct()" instead';
$phpcsFile->addError($error, $nextNonEmpty, 'OldStyleCall');
}
$startIndex = $nextNonEmpty;
}//end while
}//end processTokenWithinScope()
/**
* Processes a token that is found within the scope that this test is
* listening to.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
* @param int $stackPtr The position in the stack where this
* token was found.
*
* @return void
*/
protected function processTokenOutsideScope(File $phpcsFile, $stackPtr)
{
}//end processTokenOutsideScope()
/**
* Extracts all the function names found in the given scope.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being scanned.
* @param int $currScope A pointer to the start of the scope.
*
* @return void
*/
protected function loadFunctionNamesInScope(File $phpcsFile, $currScope)
{
$this->functionList = [];
$tokens = $phpcsFile->getTokens();
for ($i = ($tokens[$currScope]['scope_opener'] + 1); $i < $tokens[$currScope]['scope_closer']; $i++) {
if ($tokens[$i]['code'] !== T_FUNCTION) {
continue;
}
$this->functionList[] = trim(strtolower($phpcsFile->getDeclarationName($i)));
if (isset($tokens[$i]['scope_closer']) !== false) {
// Skip past nested functions and such.
$i = $tokens[$i]['scope_closer'];
}
}
}//end loadFunctionNamesInScope()
}//end class
@@ -0,0 +1,55 @@
<?php
/**
* Checks that interfaces are suffixed by Interface.
*
* @author Anna Borzenko <annnechko@gmail.com>
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class InterfaceNameSuffixSniff implements Sniff
{
/**
* Registers the tokens that this sniff wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_INTERFACE];
}//end register()
/**
* Processes this sniff, 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$interfaceName = $phpcsFile->getDeclarationName($stackPtr);
if ($interfaceName === null) {
// Live coding or parse error. Bow out.
return;
}
$suffix = substr($interfaceName, -9);
if (strtolower($suffix) !== 'interface') {
$phpcsFile->addError('Interface names must be suffixed with "Interface"; found "%s"', $stackPtr, 'Missing', [$interfaceName]);
}
}//end process()
}//end class
@@ -0,0 +1,55 @@
<?php
/**
* Checks that traits are suffixed by Trait.
*
* @author Anna Borzenko <annnechko@gmail.com>
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
class TraitNameSuffixSniff implements Sniff
{
/**
* Registers the tokens that this sniff wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_TRAIT];
}//end register()
/**
* Processes this sniff, 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$traitName = $phpcsFile->getDeclarationName($stackPtr);
if ($traitName === null) {
// Live coding or parse error. Bow out.
return;
}
$suffix = substr($traitName, -5);
if (strtolower($suffix) !== 'trait') {
$phpcsFile->addError('Trait names must be suffixed with "Trait"; found "%s"', $stackPtr, 'Missing', [$traitName]);
}
}//end process()
}//end class
@@ -0,0 +1,151 @@
<?php
/**
* Ensures that constant names are all uppercase.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class UpperCaseConstantNameSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_STRING,
T_CONST,
];
}//end register()
/**
* 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 passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_CONST) {
// This is a constant declared with the "const" keyword.
// This may be an OO constant, in which case it could be typed, so we need to
// jump over a potential type to get to the name.
$assignmentOperator = $phpcsFile->findNext([T_EQUAL, T_SEMICOLON], ($stackPtr + 1));
if ($assignmentOperator === false || $tokens[$assignmentOperator]['code'] !== T_EQUAL) {
// Parse error/live coding. Nothing to do. Rest of loop is moot.
return;
}
$constant = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($assignmentOperator - 1), ($stackPtr + 1), true);
if ($constant === false) {
return;
}
$constName = $tokens[$constant]['content'];
if (strtoupper($constName) !== $constName) {
if (strtolower($constName) === $constName) {
$phpcsFile->recordMetric($constant, 'Constant name case', 'lower');
} else {
$phpcsFile->recordMetric($constant, 'Constant name case', 'mixed');
}
$error = 'Class constants must be uppercase; expected %s but found %s';
$data = [
strtoupper($constName),
$constName,
];
$phpcsFile->addError($error, $constant, 'ClassConstantNotUpperCase', $data);
} else {
$phpcsFile->recordMetric($constant, 'Constant name case', 'upper');
}
return;
}//end if
// Only interested in define statements now.
if (strtolower($tokens[$stackPtr]['content']) !== 'define') {
return;
}
// Make sure this is not a method call or class instantiation.
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($tokens[$prev]['code'] === T_OBJECT_OPERATOR
|| $tokens[$prev]['code'] === T_DOUBLE_COLON
|| $tokens[$prev]['code'] === T_NULLSAFE_OBJECT_OPERATOR
|| $tokens[$prev]['code'] === T_NEW
) {
return;
}
// Make sure this is not an attribute.
if (empty($tokens[$stackPtr]['nested_attributes']) === false) {
return;
}
// If the next non-whitespace token after this token
// is not an opening parenthesis then it is not a function call.
$openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($openBracket === false || $tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) {
return;
}
// Bow out if next non-empty token after the opening parenthesis is not a string (the
// constant name). This could happen when live coding, if the constant is a variable or an
// expression, or if handling a first-class callable or a function definition outside the
// global scope.
$constPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($openBracket + 1), null, true);
if ($constPtr === false || $tokens[$constPtr]['code'] !== T_CONSTANT_ENCAPSED_STRING) {
return;
}
$constName = $tokens[$constPtr]['content'];
$prefix = '';
// Strip namespace from constant like /foo/bar/CONSTANT.
$splitPos = strrpos($constName, '\\');
if ($splitPos !== false) {
$prefix = substr($constName, 0, ($splitPos + 1));
$constName = substr($constName, ($splitPos + 1));
}
if (strtoupper($constName) !== $constName) {
if (strtolower($constName) === $constName) {
$phpcsFile->recordMetric($constPtr, 'Constant name case', 'lower');
} else {
$phpcsFile->recordMetric($constPtr, 'Constant name case', 'mixed');
}
$error = 'Constants must be uppercase; expected %s but found %s';
$data = [
$prefix.strtoupper($constName),
$prefix.$constName,
];
$phpcsFile->addError($error, $constPtr, 'ConstantNotUpperCase', $data);
} else {
$phpcsFile->recordMetric($constPtr, 'Constant name case', 'upper');
}
}//end process()
}//end class

Some files were not shown because too many files have changed in this diff Show More