├── .docker └── validate-against-schema │ └── Dockerfile ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── composer.json └── src └── Unleashed ├── Helpers ├── NamespaceHelper.php ├── SniffLocalCache.php └── UseStatements.php ├── Sniffs ├── Commenting │ ├── ForbiddenSingleLineCommentsSniff.php │ └── InheritDocFormatSniff.php ├── DoctrineMigrations │ └── DescriptionRequiredSniff.php ├── Namespaces │ └── FullyQualifiedGlobalFunctionsSniff.php └── PHP │ └── ForbiddenClassesSniff.php └── ruleset.xml /.docker/validate-against-schema/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.17.3 2 | 3 | RUN apk add --no-cache libxml2-utils=2.10.4-r0 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Although this coding standard is primarily for Unleashed Technologies' internal use, bug fixes are welcome and feature requests may be considered. 4 | 5 | 1. Fork it 6 | 2. Create your feature branch (`git checkout -b my-new-feature`) 7 | 3. Commit your changes (`git commit -am 'Add some feature'`) 8 | 4. [Add tests for your changes](https://github.com/unleashedtech/php-coding-standard/blob/master/TESTING.md) 9 | 5. Push your changes to your feature branch (`git push origin my-new-feature`) 10 | 6. Create a new Pull Request (PR) 11 | 12 | ## Testing 13 | Contributions will only be accepted if they are fully tested as specified in [TESTING.md](https://github.com/unleashedtech/php-coding-standard/blob/master/TESTING.md). 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Unleashed Technologies 4 | 5 | Forked from doctrine/coding-standard, Copyright (c) 2013 Steve Müller 6 | 7 | FullyQualifiedGlobalFunctionsSniff based on soderlind/coding-standard, Copyright (c) 2020 Per Søderlind 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test phpcs test-report test-fix 2 | 3 | test: test-report test-fix 4 | 5 | phpcs: 6 | ./bin/phpcs 7 | 8 | test-report: vendor 9 | ./bin/test-report 10 | 11 | test-fix: vendor 12 | ./bin/test-fix 13 | 14 | vendor: composer.json 15 | composer update 16 | touch -c vendor 17 | 18 | update-tests-to-latest-standards: vendor 19 | ./bin/update-tests-to-latest-standards 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unleashed Technologies PHP_CodeSniffer Coding Standard 2 | 3 | [![Latest Version](https://img.shields.io/packagist/v/unleashedtech/php-coding-standard.svg?style=flat-square)](https://packagist.org/packages/unleashedtech/php-coding-standard) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/unleashedtech/php-coding-standard.svg?style=flat-square)](https://packagist.org/packages/unleashedtech/php-coding-standard) 5 | [![Build Status](https://img.shields.io/travis/unleashedtech/php-coding-standard/master.svg?style=flat-square)](https://travis-ci.org/unleashedtech/php-coding-standard) 6 | [![Software License](https://img.shields.io/badge/License-MIT-brightgreen.svg?style=flat-square)](LICENSE) 7 | 8 | A PHP coding standard for Unleashed Technologies, originally based on [doctrine/coding-standard](https://github.com/doctrine/coding-standard). 9 | 10 | ## Overview 11 | 12 | This coding standard is based on [PSR-1](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md) and [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md), 13 | with some noticeable exceptions/differences/extensions based on best-practices adopted by Symfony, Doctrine, and the wider community: 14 | 15 | - Keep the nesting of control structures per method as small as possible 16 | - Add spaces around a concatenation operator ``$foo = 'Hello ' . 'World!';`` 17 | - Add spaces between assignment, control and return statements 18 | - Add spaces after the colon in return type declaration ``function (): void {}`` 19 | - Add spaces after a type cast ``$foo = (int) '12345';`` 20 | - Use single-quotes for enclosing strings 21 | - Always use strict comparisons 22 | - Always add ``declare(strict_types=1)`` at the beginning of a file 23 | - Always add native types where possible 24 | - Omit phpDoc for parameters/returns with native types, unless adding description 25 | - Don't use ``@author``, ``@since`` and similar annotations that duplicate Git information 26 | - Use parentheses when creating new instances that do not require arguments ``$foo = new Foo()`` 27 | - Use Null Coalesce Operator ``$foo = $bar ?? $baz`` 28 | - Use Null Safe Object Operator ``$foo = $object?->property`` 29 | - Prefer early exit over nesting conditions or using else 30 | - Always use fully-qualified global functions (without needing `use function` statements) 31 | - Forbids the use of `\DateTime` 32 | 33 | For full reference of enforcements, go through ``src/Unleashed/ruleset.xml`` where each sniff is briefly described. 34 | 35 | ## Installation 36 | 37 | You can install the Unleashed Coding Standard as a [Composer](https://getcomposer.org/) dependency in your project: 38 | 39 | ```sh 40 | composer require --dev unleashedtech/php-coding-standard 41 | ``` 42 | 43 | Then you can use it like this: 44 | 45 | ```sh 46 | vendor/bin/phpcs --standard=Unleashed /path/to/some/files.php 47 | ``` 48 | 49 | You can also use `phpcbp` to automatically find and fix any violations: 50 | 51 | ```sh 52 | vendor/bin/phpcbf --standard=Unleashed /path/to/some/files.php 53 | ``` 54 | 55 | ### Project-Level Ruleset 56 | 57 | To enable the Unleashed Coding Standard for your project, create a `phpcs.xml.dist` file with the following content: 58 | 59 | ```xml 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | src 73 | tests 74 | 75 | 76 | 77 | 78 | ``` 79 | 80 | 81 | This will enable the full Unleashed Coding Standard with all rules included with their defaults. 82 | From now on you can just run `vendor/bin/phpcs` and `vendor/bin/phpcbf` without any arguments. 83 | 84 | Don't forget to add `.phpcs-cache` and `phpcs.xml` (without `.dist` suffix) to your `.gitignore`. 85 | The first ignored file is a cache used by PHP CodeSniffer to speed things up, 86 | the second one allows any developer to adjust configuration locally without touching the versioned file. 87 | 88 | For further reading about the CodeSniffer configuration, please refer to 89 | [the configuration format overview](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-Ruleset) 90 | and [the list of configuration options](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Configuration-Options). 91 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unleashedtech/php-coding-standard", 3 | "type": "phpcodesniffer-standard", 4 | "description": "CodeSniffer ruleset used by Unleashed Technologies", 5 | "keywords": ["coding standard", "phpcs"], 6 | "homepage": "https://github.com/unleashedtech/php-coding-standard", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Colin O'Dell", 11 | "email": "codell@unleashed-technologies.com" 12 | } 13 | ], 14 | "extra": { 15 | "branch-alias": { 16 | "dev-master": "3.0.x-dev" 17 | } 18 | }, 19 | "support" : { 20 | "source": "https://github.com/unleashedtech/php-coding-standard", 21 | "issues": "https://github.com/unleashedtech/php-coding-standard/issues" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Unleashed\\": "src/Unleashed" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Unleashed\\Tests\\": "tests" 31 | } 32 | }, 33 | "require": { 34 | "php": "^8.1", 35 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0.0", 36 | "slevomat/coding-standard": "^8.11", 37 | "squizlabs/php_codesniffer": "^3.7" 38 | }, 39 | "require-dev": { 40 | "symfony/phpunit-bridge": "^6.4" 41 | }, 42 | "scripts": { 43 | "test": [ 44 | "@phpcs", 45 | "./vendor/bin/simple-phpunit", 46 | "make test" 47 | ], 48 | "phpcs": "./vendor/bin/phpcs" 49 | }, 50 | "config": { 51 | "allow-plugins": { 52 | "dealerdirect/phpcodesniffer-composer-installer": true 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Unleashed/Helpers/NamespaceHelper.php: -------------------------------------------------------------------------------- 1 | findNext(T_NAMESPACE, 0); 20 | 21 | return $token === false ? null : $token; 22 | }; 23 | 24 | return SniffLocalCache::getAndSetIfNotCached($phpcsFile, 'firstNamespacePointer', $lazyValue); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Unleashed/Helpers/SniffLocalCache.php: -------------------------------------------------------------------------------- 1 | > */ 17 | private static array $cache = []; 18 | 19 | public static function getAndSetIfNotCached(File $phpcsFile, string $key, \Closure $lazyValue): mixed 20 | { 21 | $fixerLoops = $phpcsFile->fixer?->loops ?? 0; 22 | $internalKey = \sprintf('%s-%s', $phpcsFile->getFilename(), $key); 23 | 24 | self::setIfNotCached($fixerLoops, $internalKey, $lazyValue); 25 | 26 | return self::$cache[$fixerLoops][$internalKey] ?? null; 27 | } 28 | 29 | private static function setIfNotCached(int $fixerLoops, string $internalKey, \Closure $lazyValue): void 30 | { 31 | if ( 32 | \array_key_exists($fixerLoops, self::$cache) && 33 | \array_key_exists($internalKey, self::$cache[$fixerLoops]) 34 | ) { 35 | return; 36 | } 37 | 38 | self::$cache[$fixerLoops][$internalKey] = $lazyValue(); 39 | 40 | if ($fixerLoops > 0) { 41 | unset(self::$cache[$fixerLoops - 1]); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Unleashed/Helpers/UseStatements.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static function getAliasesAndNonGlobalFunctionsDefinedInUseStatements(File $file): array 20 | { 21 | $lazyValue = static function () use ($file): array { 22 | $result = []; 23 | 24 | foreach (UseStatementHelper::getFileUseStatements($file) as $useStatements) { 25 | foreach ($useStatements as $useStatement) { 26 | \assert($useStatement instanceof UseStatement); 27 | if ($useStatement->getType() !== 'function') { 28 | continue; 29 | } 30 | 31 | if ( 32 | $useStatement->getAlias() !== null 33 | || \strpos($useStatement->getFullyQualifiedTypeName(), '\\') !== false 34 | ) { 35 | $result[$useStatement->getCanonicalNameAsReferencedInFile()] = true; 36 | } 37 | } 38 | } 39 | 40 | return $result; 41 | }; 42 | 43 | return SniffLocalCache::getAndSetIfNotCached($file, __METHOD__, $lazyValue); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Unleashed/Sniffs/Commenting/ForbiddenSingleLineCommentsSniff.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function register(): array 22 | { 23 | return [ 24 | T_COMMENT, 25 | ]; 26 | } 27 | 28 | /** 29 | * @param int $stackPtr 30 | * 31 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint 32 | */ 33 | public function process(File $phpcsFile, $stackPtr): void 34 | { 35 | $tokens = $phpcsFile->getTokens(); 36 | 37 | $content = $tokens[$stackPtr]['content']; 38 | 39 | foreach (SniffSettingsHelper::normalizeArray($this->forbiddenCommentPatterns) as $forbiddenCommentPattern) { 40 | if (! SniffSettingsHelper::isValidRegularExpression($forbiddenCommentPattern)) { 41 | throw new \Exception(\sprintf('%s is not valid PCRE pattern.', $forbiddenCommentPattern)); 42 | } 43 | 44 | if (\preg_match($forbiddenCommentPattern, $content) === 0) { 45 | continue; 46 | } 47 | 48 | $fix = $phpcsFile->addFixableError( 49 | \sprintf('Code contains forbidden comment "%s".', \trim($content)), 50 | $stackPtr, 51 | self::CODE_COMMENT_FORBIDDEN, 52 | ); 53 | 54 | if (! $fix) { 55 | continue; 56 | } 57 | 58 | $phpcsFile->fixer->beginChangeset(); 59 | $fixedContent = \preg_replace($forbiddenCommentPattern, '', $content); 60 | $phpcsFile->fixer->replaceToken($stackPtr, $fixedContent); 61 | $phpcsFile->fixer->endChangeset(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Unleashed/Sniffs/Commenting/InheritDocFormatSniff.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function register(): array 23 | { 24 | return [ 25 | T_DOC_COMMENT_OPEN_TAG, 26 | ]; 27 | } 28 | 29 | /** 30 | * @param int $stackPtr 31 | * 32 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint 33 | */ 34 | public function process(File $phpcsFile, $stackPtr): void 35 | { 36 | $tokens = $phpcsFile->getTokens(); 37 | 38 | for ($i = $stackPtr + 1; $i < $tokens[$stackPtr]['comment_closer']; $i++) { 39 | if (\in_array($tokens[$i]['code'], [T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR], true)) { 40 | continue; 41 | } 42 | 43 | $content = $tokens[$i]['content']; 44 | 45 | if (\preg_match('~^(?:{@inheritDoc}|@inheritDoc)$~i', $content) === 0) { 46 | continue; 47 | } 48 | 49 | $fixed = \preg_replace('~({@inheritDoc}|@inheritDoc)~i', $this->style, $content); 50 | if ($content === $fixed) { 51 | continue; 52 | } 53 | 54 | $fix = $phpcsFile->addFixableError( 55 | \sprintf('Incorrect formatting of "%s"', $this->style), 56 | $i, 57 | self::CODE_INVALID_INHERITDOC_STYLE, 58 | ); 59 | 60 | if (! $fix) { 61 | return; 62 | } 63 | 64 | $phpcsFile->fixer->beginChangeset(); 65 | $phpcsFile->fixer->replaceToken($i, $fixed); 66 | $phpcsFile->fixer->endChangeset(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Unleashed/Sniffs/DoctrineMigrations/DescriptionRequiredSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 35 | 36 | $parentClassPtr = TokenHelper::findNext($phpcsFile, T_STRING, $stackPtr); 37 | if ($parentClassPtr === null) { 38 | return; 39 | } 40 | 41 | // Only continue if we've found a Doctrine Migration class 42 | $parentClass = NamespaceHelper::resolveClassName($phpcsFile, $tokens[$parentClassPtr]['content'], $stackPtr); 43 | if ($parentClass !== '\\Doctrine\\Migrations\\AbstractMigration') { 44 | return; 45 | } 46 | 47 | // Does a `getDescription()` method exist? 48 | $classEndPtr = self::findApproximateClassEndPointer($phpcsFile, $parentClassPtr); 49 | $methodPtr = self::findMethodInClass($phpcsFile, 'getDescription', $parentClassPtr, $classEndPtr); 50 | if ($methodPtr === null) { 51 | // Nope - method is missing 52 | $fix = $phpcsFile->addFixableError( 53 | 'Doctrine Migrations must have a getDescription() method.', 54 | TokenHelper::findPrevious($phpcsFile, T_CLASS, $stackPtr), 55 | self::CODE_MISSING_DESCRIPTION, 56 | ); 57 | 58 | if ($fix) { 59 | $phpcsFile->fixer->beginChangeset(); 60 | $phpcsFile->fixer->addContent( 61 | TokenHelper::findNext($phpcsFile, T_OPEN_CURLY_BRACKET, $stackPtr), 62 | "\n public function getDescription(): string\n {\n return '';\n }", 63 | ); 64 | $phpcsFile->fixer->endChangeset(); 65 | } 66 | 67 | return; 68 | } 69 | 70 | // Does the method have a description? 71 | $returnValuePtr = $phpcsFile->findNext(T_CONSTANT_ENCAPSED_STRING, $methodPtr + 1, null, false, null, true); 72 | if ($tokens[$returnValuePtr]['content'] !== '""' && $tokens[$returnValuePtr]['content'] !== "''") { 73 | return; 74 | } 75 | 76 | $phpcsFile->addError( 77 | 'Doctrine Migrationss must return useful information from getDescription(); empty strings not allowed', 78 | $returnValuePtr, 79 | self::CODE_EMPTY_DESCRIPTION, 80 | ); 81 | } 82 | 83 | private static function findMethodInClass(File $phpcsFile, string $methodName, int $startPtr, int $endPtr): int|null 84 | { 85 | do { 86 | $nextFunctionPointer = TokenHelper::findNext($phpcsFile, T_FUNCTION, $startPtr + 1); 87 | if ($nextFunctionPointer === null) { 88 | break; 89 | } 90 | 91 | if ($nextFunctionPointer >= $endPtr) { 92 | return null; 93 | } 94 | 95 | $startPtr = $nextFunctionPointer; 96 | 97 | if (! FunctionHelper::isMethod($phpcsFile, $startPtr)) { 98 | continue; 99 | } 100 | 101 | $name = FunctionHelper::getName($phpcsFile, $nextFunctionPointer); 102 | 103 | if ($name !== $methodName) { 104 | continue; 105 | } 106 | 107 | return $nextFunctionPointer; 108 | } while (true); 109 | 110 | return null; 111 | } 112 | 113 | private static function findApproximateClassEndPointer(File $phpcsFile, int $ptrWithinClass): int|null 114 | { 115 | $classPtrs = \array_keys(ClassHelper::getAllNames($phpcsFile)); 116 | 117 | while (\current($classPtrs) !== false) { 118 | $start = \current($classPtrs); 119 | $end = \next($classPtrs); 120 | 121 | if ($start <= $ptrWithinClass && $end >= $ptrWithinClass) { 122 | return $end; 123 | } 124 | } 125 | 126 | return $phpcsFile->numTokens; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Unleashed/Sniffs/Namespaces/FullyQualifiedGlobalFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | */ 18 | private array $optimizedFunctions = [ 19 | // @see https://github.com/php/php-src/blob/PHP-7.4/Zend/zend_compile.c "zend_try_compile_special_func" 20 | 'array_key_exists' => true, 21 | 'array_slice' => true, 22 | 'assert' => true, 23 | 'boolval' => true, 24 | 'call_user_func' => true, 25 | 'call_user_func_array' => true, 26 | 'chr' => true, 27 | 'count' => true, 28 | 'defined' => true, 29 | 'doubleval' => true, 30 | 'floatval' => true, 31 | 'func_get_args' => true, 32 | 'func_num_args' => true, 33 | 'get_called_class' => true, 34 | 'get_class' => true, 35 | 'gettype' => true, 36 | 'in_array' => true, 37 | 'intval' => true, 38 | 'is_array' => true, 39 | 'is_bool' => true, 40 | 'is_double' => true, 41 | 'is_float' => true, 42 | 'is_int' => true, 43 | 'is_integer' => true, 44 | 'is_long' => true, 45 | 'is_null' => true, 46 | 'is_object' => true, 47 | 'is_real' => true, 48 | 'is_resource' => true, 49 | 'is_string' => true, 50 | 'ord' => true, 51 | 'sizeof' => true, 52 | 'strlen' => true, 53 | 'strval' => true, 54 | // @see https://github.com/php/php-src/blob/php-7.2.6/ext/opcache/Optimizer/pass1_5.c 55 | 'constant' => true, 56 | 'define' => true, 57 | 'dirname' => true, 58 | 'extension_loaded' => true, 59 | 'function_exists' => true, 60 | 'is_callable' => true, 61 | ]; 62 | 63 | /** 64 | * Returns an array of tokens this test wants to listen for. 65 | * We're looking for all functions, so use T_STRING. 66 | * 67 | * {@inheritDoc} 68 | */ 69 | public function register() 70 | { 71 | return [\T_STRING]; 72 | } 73 | 74 | /** 75 | * Processes this test, when one of its tokens is encountered. 76 | * 77 | * Code from ForbiddenFunctionsSniff: 78 | * 79 | * @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php#L118 80 | * 81 | * @param int $stackPtr 82 | * 83 | * {@inheritDoc} 84 | * 85 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint 86 | */ 87 | public function process(File $phpcsFile, $stackPtr): void 88 | { 89 | // Abort if we're not in namespaced code 90 | $firstNamespacePointer = NamespaceHelper::getFirstNamespacePointer($phpcsFile); 91 | if ($firstNamespacePointer === null || $stackPtr < $firstNamespacePointer) { 92 | return; 93 | } 94 | 95 | if ( 96 | $this->onlyOptimizedFunctions !== null 97 | && \filter_var($this->onlyOptimizedFunctions, FILTER_VALIDATE_BOOLEAN) !== false 98 | ) { 99 | $globalFunctions = $this->optimizedFunctions; 100 | } else { 101 | $globalFunctions = \array_flip(\get_defined_functions()['internal']); 102 | } 103 | 104 | $whitelist = UseStatements::getAliasesAndNonGlobalFunctionsDefinedInUseStatements($phpcsFile); 105 | 106 | $tokens = $phpcsFile->getTokens(); 107 | $ignore = [ 108 | T_DOUBLE_COLON => true, 109 | T_OBJECT_OPERATOR => true, 110 | T_FUNCTION => true, 111 | T_CONST => true, 112 | T_PUBLIC => true, 113 | T_PRIVATE => true, 114 | T_PROTECTED => true, 115 | T_AS => true, 116 | T_NEW => true, 117 | T_INSTEADOF => true, 118 | T_NS_SEPARATOR => true, 119 | T_IMPLEMENTS => true, 120 | ]; 121 | $prevToken = $phpcsFile->findPrevious([T_WHITESPACE, T_COMMENT], $stackPtr - 1, null, true); 122 | 123 | // If function call is directly preceded by a NS_SEPARATOR don't try to fix it. 124 | if ($tokens[$prevToken]['code'] === T_NS_SEPARATOR && $tokens[$stackPtr]['code'] === T_STRING) { 125 | return; 126 | } 127 | 128 | if (isset($ignore[$tokens[$prevToken]['code']]) === true) { 129 | // Not a call to a PHP function. 130 | return; 131 | } 132 | 133 | $nextToken = $phpcsFile->findNext([T_WHITESPACE, T_COMMENT], $stackPtr + 1, null, true); 134 | if (isset($ignore[$tokens[$nextToken]['code']]) === true) { 135 | // Not a call to a PHP function. 136 | return; 137 | } 138 | 139 | if ($tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS) { 140 | // Not a call to a PHP function. 141 | return; 142 | } 143 | 144 | $function = \strtolower($tokens[$stackPtr]['content']); 145 | $functionNormalized = \strtolower($function); 146 | 147 | // Is it a whitelisted alias? 148 | if (isset($whitelist[$functionNormalized])) { 149 | return; 150 | } 151 | 152 | // Is it an global PHP function? 153 | if (isset($globalFunctions[$functionNormalized]) === false) { 154 | return; 155 | } 156 | 157 | $error = \sprintf('Function %1$s() should be referenced via a fully qualified name, e.g.: \%1$s()', $function); 158 | $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NotFullyQualified'); 159 | 160 | if ($fix === true) { 161 | $this->applyFix($phpcsFile, $stackPtr, $function); 162 | } 163 | } 164 | 165 | private function applyFix(File $phpcsFile, int $stackPtr, string $function): void 166 | { 167 | // This sniff conflicts with ModernClassNameReferenceSniff, so don't bother fixing things it will attempt to fix 168 | if ( 169 | \array_key_exists(ModernClassNameReferenceSniff::class, $phpcsFile->ruleset->sniffs) 170 | && \in_array($function, ['get_class', 'get_parent_class', 'get_called_class'], true) 171 | ) { 172 | return; 173 | } 174 | 175 | $phpcsFile->fixer->beginChangeset(); 176 | $phpcsFile->fixer->addContentBefore($stackPtr, '\\'); 177 | $phpcsFile->fixer->endChangeset(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Unleashed/Sniffs/PHP/ForbiddenClassesSniff.php: -------------------------------------------------------------------------------- 1 | getStartPointer(); 56 | $name = $referencedName->getNameAsReferencedInFile(); 57 | 58 | $fullyQualifiedName = NamespaceHelper::resolveClassName($phpcsFile, $name, $pointer); 59 | 60 | if (! \in_array($fullyQualifiedName, $this->forbiddenClasses, true)) { 61 | continue; 62 | } 63 | 64 | $error = \sprintf('The use of "%s" is forbidden', $fullyQualifiedName); 65 | if ($this->error) { 66 | $phpcsFile->addError($error, $pointer, self::FORBIDDEN); 67 | } else { 68 | $phpcsFile->addWarning($error, $pointer, self::FORBIDDEN); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Unleashed/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | The Unleashed Technologies PHP coding standard. 8 | 9 | 10 | *.css 11 | *.js 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | error 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 204 | 205 | 210 | 211 | 212 | 213 | 214 | 215 | 220 | 221 | 229 | 235 | 236 | 242 | 243 | 244 | 245 | 250 | 255 | 256 | 264 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 481 | 485 | 489 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | Variable "%s" not allowed in double quoted string; use sprintf() or concatenation instead 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 5 719 | 720 | 721 | --------------------------------------------------------------------------------