├── LICENSE ├── README.md ├── composer.json ├── ecs ├── examples ├── InvalidConstructs.php ├── ValidClass.php ├── ValidConstructs.php ├── ValidInterface.php ├── ValidPresenter.php ├── ValidPropertyAndMethodSpacing.php ├── empty-file.php └── functions.php ├── preset-fixer ├── base.php ├── clean-code.php ├── common │ ├── Nette.php │ └── replaces.php ├── php71.php ├── php72.php ├── php73.php ├── php74.php ├── php80.php ├── php81.php ├── php82.php ├── php83.php ├── php84.php └── types.php ├── preset-sniffer ├── Nette.xml ├── clean-code.xml ├── php71.xml ├── php72.xml ├── php73.xml ├── php74.xml ├── php80.xml ├── php81.xml ├── php82.xml ├── php83.xml ├── php84.xml └── types.xml ├── run.php └── src ├── Checker.php ├── Fixer ├── BracesPositionFixer.php ├── ClassAndTraitVisibilityRequiredFixer.php ├── MethodArgumentSpaceFixer.php └── StatementIndentationFixer.php └── NetteCodingStandard ├── Sniffs ├── Namespaces │ └── OptimizeGlobalCallsSniff.php └── WhiteSpace │ └── FunctionSpacingSniff.php └── ruleset.xml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nette Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nette Coding Standard code checker & fixer 2 | 3 | [![Downloads this Month](https://img.shields.io/packagist/dm/nette/coding-standard.svg)](https://packagist.org/packages/nette/coding-standard) 4 | [![Latest Stable Version](https://img.shields.io/packagist/v/nette/coding-standard.svg)](https://github.com/nette/coding-standard/releases) 5 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) 6 | 7 | 8 | This is set of [sniffs](https://github.com/squizlabs/PHP_CodeSniffer) and [fixers](https://github.com/FriendsOfPHP/PHP-CS-Fixer) that **checks and fixes** code of Nette Framework against [Coding Standard in Documentation](https://doc.nette.org/en/contributing/coding-standard). 9 | 10 | 11 | ## Installation and Usage 12 | 13 | Install the tool in a global directory. Its name will be for example `/nette-cs`: 14 | 15 | ``` 16 | composer create-project nette/coding-standard /nette-cs 17 | ``` 18 | 19 | Check coding standard for PHP 7.1 in folders `src` and `tests`: 20 | 21 | ```bash 22 | /nette-cs/ecs check src tests --preset php71 23 | ``` 24 | 25 | And fix it: 26 | 27 | ```bash 28 | /nette-cs/ecs fix src tests --preset php71 29 | ``` 30 | 31 | If no PHP version is specified, it will try to find out from the `composer.json` file. 32 | 33 | 34 | ### GitHub Actions 35 | 36 | ```yaml 37 | # .github/workflows/coding-style.yml 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: shivammathur/setup-php@v2 41 | with: 42 | php-version: 8.0 43 | 44 | - run: composer create-project nette/coding-standard temp/coding-standard 45 | - run: php temp/coding-standard/ecs check src tests --preset php71 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nette/coding-standard", 3 | "license": "MIT", 4 | "require": { 5 | "php": "^8.0", 6 | "slevomat/coding-standard": "^8.16", 7 | "friendsofphp/php-cs-fixer": "^3.68", 8 | "squizlabs/php_codesniffer": "^3.11", 9 | "kubawerlos/php-cs-fixer-custom-fixers": "^3.22" 10 | }, 11 | "require-dev": { 12 | "tracy/tracy": "^2.5", 13 | "nette/tester": "^2.5" 14 | }, 15 | "autoload": { 16 | "classmap": ["src/"] 17 | }, 18 | "bin": ["ecs"], 19 | "scripts": { 20 | "tester": "tester tests -s" 21 | }, 22 | "extra": { 23 | "branch-alias": { 24 | "dev-master": "3-dev" 25 | } 26 | }, 27 | "config": { 28 | "allow-plugins": { 29 | "dealerdirect/phpcodesniffer-composer-installer": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ecs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $a + 2 ); 40 | 41 | 42 | func($a 43 | ? $b 44 | : $c 45 | ); 46 | 47 | 48 | func($a && ($a 49 | ? $b 50 | : $c) && $c, $d 51 | ); 52 | 53 | 54 | if($a && ($a 55 | ? $b 56 | : $c) && $c 57 | ) {echo 1;} 58 | 59 | 60 | 61 | if ($this->lastAttrValue === '' && $this->context && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE)) { 62 | x(); 63 | } 64 | 65 | if ($tokens->isNext() && (!$tokens->isNext($tokens::T_CHAR) || $tokens->isNext('hfklasdehfgisdgfkljhsnettefsedhgfsdghflskdhf', '\\'))) { 66 | x(); 67 | } 68 | 69 | if ( 70 | $tokens->isNext() 71 | && ($tokens->isNext($tokens::T_CHAR) 72 | || $tokens->isNext('hfklasdehfgisdgfkljhsnettefsedhgfsdghflskdhf', '\\')) 73 | ) { 74 | x(); 75 | } 76 | 77 | 78 | $s .= ($item['hfklasdehfgisdgfkljhsnettefsedhgfsdghflskdhfsdlhfgldkshsdfhgsdlkfh'] 79 | ? ( 80 | $a . $b 81 | ) 82 | : ($line 83 | ? trim($line) 84 | : $item 85 | ) 86 | ); 87 | 88 | $s .= fnc($item['hfklasdehfgisdgfkljhsnettefsedhgfsdghflskdhfsdlhfgldkshsdfhgsdlkfh'] 89 | ? ( 90 | $a . $b 91 | ) 92 | : ($line 93 | ? trim($line) 94 | : $item 95 | ) 96 | ); 97 | -------------------------------------------------------------------------------- /examples/ValidClass.php: -------------------------------------------------------------------------------- 1 | $a + 2); 43 | 44 | 45 | func( 46 | $a 47 | ? $b 48 | : $c 49 | ); 50 | 51 | 52 | func( 53 | $a && ($a 54 | ? $b 55 | : $c) && $c, 56 | $d 57 | ); 58 | 59 | 60 | if ($a && ($a 61 | ? $b 62 | : $c) && $c 63 | ) { 64 | echo 1; 65 | } 66 | 67 | 68 | 69 | if ( 70 | $this->lastAttrValue === '' 71 | && $this->context 72 | && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE) 73 | ) { 74 | x(); 75 | } 76 | 77 | if ( 78 | $tokens->isNext() 79 | && ( 80 | !$tokens->isNext($tokens::T_CHAR) 81 | || $tokens->isNext('hfklasdehfgisdgfkljhsnettefsedhgfsdghflskdhf', '\\') 82 | ) 83 | ) { 84 | x(); 85 | } 86 | 87 | if ( 88 | $tokens->isNext() 89 | && ($tokens->isNext($tokens::T_CHAR) 90 | || $tokens->isNext('hfklasdehfgisdgfkljhsnettefsedhgfsdghflskdhf', '\\')) 91 | ) { 92 | x(); 93 | } 94 | 95 | 96 | $s .= ($item['hfklasdehfgisdgfkljhsnettefsedhgfsdghflskdhfsdlhfgldkshsdfhgsdlkfh'] 97 | ? ( 98 | $a . $b 99 | ) 100 | : ($line 101 | ? trim($line) 102 | : $item 103 | ) 104 | ); 105 | 106 | $s .= fnc( 107 | $item['hfklasdehfgisdgfkljhsnettefsedhgfsdghflskdhfsdlhfgldkshsdfhgsdlkfh'] 108 | ? ( 109 | $a . $b 110 | ) 111 | : ($line 112 | ? trim($line) 113 | : $item 114 | ) 115 | ); 116 | -------------------------------------------------------------------------------- /examples/ValidInterface.php: -------------------------------------------------------------------------------- 1 | $x + $y; 14 | -------------------------------------------------------------------------------- /preset-fixer/base.php: -------------------------------------------------------------------------------- 1 | new SplFileInfo($path), $files); 7 | 8 | $config = new PhpCsFixer\Config; 9 | $config->registerCustomFixers([ 10 | new NetteCodingStandard\Fixer\Whitespace\StatementIndentationFixer, 11 | new NetteCodingStandard\Fixer\Basic\BracesPositionFixer, 12 | new NetteCodingStandard\Fixer\ClassNotation\ClassAndTraitVisibilityRequiredFixer, 13 | new NetteCodingStandard\Fixer\FunctionNotation\MethodArgumentSpaceFixer, 14 | ]); 15 | $config->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()); 16 | $config->registerCustomFixers(new PhpCsFixerCustomFixers\Fixers); 17 | $config->setUsingCache(false); 18 | $config->setIndent("\t"); 19 | $config->setLineEnding(PHP_EOL); 20 | $config->setRiskyAllowed(true); 21 | $config->setFinder($files); 22 | 23 | 24 | $customRules = []; 25 | $root = getcwd(); 26 | while (!is_file("$root/ncs.php") && substr_count($root, DIRECTORY_SEPARATOR) > 1) { 27 | $root = dirname($root); 28 | } 29 | if (is_file($file = "$root/ncs.php")) { 30 | echo "used $file\n"; 31 | $customRules = require $file; 32 | } 33 | 34 | $config->setRules([]); 35 | 36 | return $config; 37 | -------------------------------------------------------------------------------- /preset-fixer/clean-code.php: -------------------------------------------------------------------------------- 1 | true, 9 | 10 | // There should not be useless `else` cases. 11 | 'no_useless_else' => true, 12 | 13 | // Internal classes should be `final` 14 | 'final_internal_class' => true, 15 | 16 | // Properties should be set to `null` instead of using `unset` 17 | 'no_unset_on_property' => true, 18 | 19 | // reformat 20 | 'no_whitespace_in_blank_line' => true, 21 | 'no_trailing_whitespace' => true, 22 | ]; 23 | 24 | $config->setRules(array_merge($rules, $customRules)); 25 | return $config; 26 | -------------------------------------------------------------------------------- /preset-fixer/common/Nette.php: -------------------------------------------------------------------------------- 1 | true, 7 | '@PSR12:risky' => true, 8 | 'new_with_parentheses' => false, // new stdClass 9 | 'single_line_after_imports' => false, // Nette uses two empty lines 10 | 'blank_line_after_namespace' => false, 11 | 'single_import_per_statement' => false, 12 | 'ordered_imports' => ['imports_order' => ['class', 'function', 'const']], 13 | 'blank_line_between_import_groups' => false, 14 | 15 | // Ensures a single space after language constructs 16 | 'single_space_around_construct' => true, 17 | // The body of each control structure MUST be enclosed within braces 18 | 'control_structure_braces' => true, 19 | // Control structure continuation keyword must be on the configured line 20 | 'control_structure_continuation_position' => true, 21 | // There must not be spaces around declare statement parentheses 22 | 'declare_parentheses' => true, 23 | // There must not be more than one statement per line 24 | 'no_multiple_statements_per_line' => true, 25 | // Spaces should be properly placed in a function declaration. 26 | 'function_declaration' => [ 27 | 'closure_fn_spacing' => 'none', 28 | ], 29 | 30 | 31 | // overriden rules 32 | 33 | // Curly braces must be placed as configured 34 | 'braces_position' => false, 35 | 'Nette/braces_position' => true, 36 | 37 | // Each statement must be indented 38 | 'statement_indentation' => false, 39 | 'Nette/statement_indentation' => ['stick_comment_to_next_continuous_control_statement' => true], 40 | 41 | // In the argument list, there must be one space after each comma, and there must no be a space before each comma 42 | 'method_argument_space' => false, 43 | 'Nette/method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 44 | 45 | 'visibility_required' => false, 46 | 'Nette/class_and_trait_visibility_required' => true, 47 | 48 | 49 | // Whitespace 50 | 51 | // Single-line whitespace before closing semicolon are prohibited 52 | 'no_singleline_whitespace_before_semicolons' => true, 53 | 54 | // Fix whitespace after a semicolon 55 | 'space_after_semicolon' => true, 56 | 57 | // Binary operators should be surrounded by at least one space. 58 | //'binary_operator_spaces' => true, 59 | 60 | // Unary operators should be placed adjacent to their operands. 61 | 'unary_operator_spaces' => true, 62 | 63 | // There MUST NOT be spaces around offset braces $a[0] 64 | 'no_spaces_around_offset' => true, 65 | 66 | // There should not be space before or after object `T_OBJECT_OPERATOR` `->`. 67 | 'object_operator_without_whitespace' => true, 68 | 69 | // Concatenation $a . $b should be spaced according configuration 70 | 'concat_space' => ['spacing' => 'one'], 71 | 72 | // A single space or none should be between cast and variable. 73 | 'cast_spaces' => true, 74 | 75 | // The namespace declaration line shouldn\'t contain leading whitespace 76 | 'no_leading_namespace_whitespace' => true, 77 | 78 | 79 | // Control structures 80 | 81 | // Ensure there is no code on the same line as the PHP open tag. 82 | 'linebreak_after_opening_tag' => true, 83 | 84 | 'no_alternative_syntax' => true, 85 | 86 | // Calling `unset` on multiple items should be done in one call. 87 | 'combine_consecutive_unsets' => true, 88 | 89 | // Replace all `<>` with `!=`. 90 | 'standardize_not_equals' => true, 91 | 92 | // Include/Require and file path should be divided with a single space. File path should not be placed under brackets. 93 | 'include' => true, 94 | 95 | // Inside a classy element "self" should be preferred to the class name itself. 96 | 'self_accessor' => true, 97 | 98 | // Function defined by PHP should be called using the correct casing 99 | 'native_function_casing' => true, 100 | 101 | // Replaces `intval`, `floatval`, `doubleval`, `strval` and `boolval` function calls with according type casting operator 102 | 'modernize_types_casting' => true, 103 | 104 | // Short cast `bool` using double exclamation mark should not be used 105 | 'no_short_bool_cast' => true, 106 | 107 | // Remove useless semicolon statements 108 | 'no_empty_statement' => true, 109 | 110 | // Removes unneeded braces that are superfluous and aren’t part of a control structure’s body 111 | 'no_unneeded_braces' => true, 112 | 113 | // Remove trailing commas in list() calls. 114 | 'no_trailing_comma_in_singleline' => true, 115 | 116 | // Removes unneeded parentheses around control statements. 117 | 'no_unneeded_control_parentheses' => true, 118 | 119 | // The structure body must be indented once. 120 | // The closing brace must be on the next line after the body. 121 | // There should not be more than one statement per line. 122 | 123 | 'no_break_comment' => [ 124 | 'comment_text' => 'break omitted', 125 | ], 126 | 127 | // Increment and decrement operators should be used if possible. 128 | 'standardize_increment' => true, 129 | 130 | // Magic constants should be referred to using the correct casing. 131 | 'magic_constant_casing' => true, 132 | 133 | 134 | // Comments 135 | 136 | // There should not be any empty comments. 137 | 'no_empty_comment' => true, 138 | 139 | // Single-line comments comments with only one line of actual content should use the `//` syntax. 140 | 'single_line_comment_style' => [ 141 | 'comment_types' => ['hash'], 142 | ], 143 | 144 | 145 | // Arrays 146 | 147 | 'no_whitespace_before_comma_in_array' => true, 148 | 'array_indentation' => true, 149 | 'trim_array_spaces' => true, 150 | 'whitespace_after_comma_in_array' => true, 151 | 152 | // commas 153 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 154 | 155 | 'array_syntax' => ['syntax' => 'short'], 156 | 157 | // $arr{} to $arr[] 158 | 'normalize_index_brace' => true, 159 | 160 | 161 | // Strings 162 | 163 | // Convert `heredoc` to `nowdoc` where possible. 164 | 'heredoc_to_nowdoc' => true, 165 | 166 | // Convert double quotes to single quotes for simple strings. 167 | 'single_quote' => true, 168 | 169 | // Handles implicit backslashes in strings and heredocs 170 | 'string_implicit_backslashes' => true, 171 | 172 | // Convert ${..} to {$..} 173 | 'simple_to_complex_string_variable' => true, 174 | 175 | 176 | // PHPDoc 177 | 178 | // Docblocks should have the same indentation as the documented subject. 179 | 'phpdoc_indent' => true, 180 | 181 | // There should not be empty PHPDoc blocks. 182 | 'no_empty_phpdoc' => true, 183 | 184 | // Phpdocs should start and end with content, excluding the very first and last line of the docblocks. 185 | 'phpdoc_trim' => true, 186 | 187 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 188 | 189 | 'phpdoc_types' => true, 190 | 191 | 192 | // Classes 193 | 194 | // class element order: constants, properties, from public to private 195 | 'ordered_class_elements' => [ 196 | 'order' => [ 197 | 'use_trait', 198 | 'constant', 199 | 'constant_public', 200 | 'constant_protected', 201 | 'constant_private', 202 | 'property_public', 203 | 'property_protected', 204 | 'property_private', 205 | ], 206 | ], 207 | 208 | // Properties MUST not be explicitly initialized with `null`. 209 | 'no_null_property_initialization' => true, 210 | 211 | // Constructor having promoted properties must have them in separate lines 212 | PhpCsFixerCustomFixers\Fixer\MultilinePromotedPropertiesFixer::name() => true, 213 | 214 | // Use the Elvis operator ?: where possible. 215 | 'ternary_to_elvis_operator' => true, 216 | 217 | // Adds or removes ? before single type declarations or |null at the end of union types when parameters have a default null value. 218 | 'nullable_type_declaration_for_default_null_value' => true, 219 | ]; 220 | -------------------------------------------------------------------------------- /preset-fixer/common/replaces.php: -------------------------------------------------------------------------------- 1 | true, 7 | //'logical_operators' => true, 8 | 'no_alias_functions' => true, 9 | 'set_type_to_cast' => true, 10 | 'combine_consecutive_issets' => true, 11 | 'combine_consecutive_unsets' => true, 12 | 'backtick_to_shell_exec' => true, 13 | 14 | // Functions should be used with `$strict` param set to `true` 15 | 'strict_param' => true, 16 | 17 | // replaces is_null(parameter) expression with `null === parameter`. 18 | 'is_null' => true, 19 | 20 | // The configured functions must be commented out 21 | PhpCsFixerCustomFixers\Fixer\CommentedOutFunctionFixer::name() => ['print_r', 'var_dump', 'var_export', 'dump'], 22 | 23 | // Classes defined internally by extension or core must be referenced with the correct case 24 | 'class_reference_name_casing' => true, 25 | 26 | // Classes in the global namespace cannot contain leading slashes 27 | PhpCsFixerCustomFixers\Fixer\NoLeadingSlashInGlobalNamespaceFixer::name() => true, 28 | ]; 29 | -------------------------------------------------------------------------------- /preset-fixer/php71.php: -------------------------------------------------------------------------------- 1 | setRules(array_merge($config->getRules(), require $file)); 9 | } 10 | 11 | $rules = [ 12 | '@PHP71Migration' => true, 13 | '@PHP70Migration:risky' => true, 14 | 'random_api_migration' => false, 15 | 'non_printable_character' => false, // not working properly 16 | ]; 17 | 18 | $config->setRules(array_merge($rules, $config->getRules(), $customRules)); 19 | return $config; 20 | -------------------------------------------------------------------------------- /preset-fixer/php72.php: -------------------------------------------------------------------------------- 1 | true, 9 | ]; 10 | 11 | $config->setRules($rules + $config->getRules()); 12 | return $config; 13 | -------------------------------------------------------------------------------- /preset-fixer/php74.php: -------------------------------------------------------------------------------- 1 | true, 9 | ]; 10 | 11 | $config->setRules($rules + $config->getRules()); 12 | return $config; 13 | -------------------------------------------------------------------------------- /preset-fixer/php80.php: -------------------------------------------------------------------------------- 1 | true, 9 | '@PHP80Migration:risky' => true, 10 | 'void_return' => false, 11 | ]; 12 | 13 | $config->setRules($rules + $config->getRules()); 14 | return $config; 15 | -------------------------------------------------------------------------------- /preset-fixer/php81.php: -------------------------------------------------------------------------------- 1 | true, 9 | ]; 10 | 11 | $config->setRules($rules + $config->getRules()); 12 | return $config; 13 | -------------------------------------------------------------------------------- /preset-fixer/php82.php: -------------------------------------------------------------------------------- 1 | true, 9 | ]; 10 | 11 | $config->setRules($rules + $config->getRules()); 12 | return $config; 13 | -------------------------------------------------------------------------------- /preset-fixer/php83.php: -------------------------------------------------------------------------------- 1 | true, 9 | ]; 10 | 11 | $config->setRules($rules + $config->getRules()); 12 | return $config; 13 | -------------------------------------------------------------------------------- /preset-fixer/php84.php: -------------------------------------------------------------------------------- 1 | true, 9 | ]; 10 | 11 | $config->setRules($rules + $config->getRules()); 12 | return $config; 13 | -------------------------------------------------------------------------------- /preset-fixer/types.php: -------------------------------------------------------------------------------- 1 | setRules(array_merge($rules, $customRules)); 11 | return $config; 12 | -------------------------------------------------------------------------------- /preset-sniffer/Nette.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 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 | 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 | 169 | 170 | 171 | Only 1 @return annotation is allowed in a function comment 172 | 173 | 174 | Extra @param annotation 175 | 176 | 177 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 0 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 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 | -------------------------------------------------------------------------------- /preset-sniffer/clean-code.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /preset-sniffer/php71.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /preset-sniffer/php72.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /preset-sniffer/php73.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /preset-sniffer/php74.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /preset-sniffer/php80.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /preset-sniffer/php81.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /preset-sniffer/php82.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /preset-sniffer/php83.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /preset-sniffer/php84.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /preset-sniffer/types.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 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 | -------------------------------------------------------------------------------- /run.php: -------------------------------------------------------------------------------- 1 | ] [path1 path2 ...]\n"; 30 | echo " check (default): Run tools in dry-run mode.\n"; 31 | echo " fix: Run tools and apply fixes.\n"; 32 | echo " --preset : Specify preset (e.g., php81). Autodetected if omitted.\n"; 33 | echo " path1 path2 ...: Specific files or directories to process. Defaults to src/, tests/ or ./\n"; 34 | exit(0); 35 | } elseif (!str_starts_with($arg, '-')) { 36 | $paths[] = $arg; 37 | } else { 38 | fwrite(STDERR, "Warning: Ignoring unknown option '{$arg}'\n"); 39 | } 40 | } 41 | 42 | 43 | // Determine Project Root (essential for finding composer.json and relative paths) 44 | $root = getcwd(); // Start from current working directory 45 | while (!is_file("$root/composer.json") && substr_count($root, DIRECTORY_SEPARATOR) > 1) { 46 | $root = dirname($root); 47 | } 48 | if (!is_file("$root/composer.json")) { 49 | $root = getcwd(); 50 | echo "Warning: Could not find composer.json, using current directory '{$root}' as project root.\n"; 51 | } 52 | 53 | 54 | 55 | // Instantiate and Configure Checker 56 | $checker = new Checker($vendorDir, $root, $dryRun, $preset); 57 | echo 'Mode: ' . ($dryRun ? 'Check (dry-run)' : 'Fix') . "\n"; 58 | 59 | // Determine and set paths 60 | $paths = $paths ?: array_filter(['src', 'tests'], 'is_dir') ?: ['.']; 61 | $checker->setPaths($paths); 62 | echo 'Paths: ' . implode(', ', $paths) . "\n"; 63 | if ($preset) { 64 | echo "Preset: {$preset}\n"; 65 | } 66 | 67 | // Signal Handling 68 | if (function_exists('pcntl_signal')) { 69 | pcntl_signal(SIGINT, function () use ($checker) { 70 | pcntl_signal(SIGINT, SIG_DFL); 71 | throw new Exception; 72 | }); 73 | } elseif (function_exists('sapi_windows_set_ctrl_handler')) { 74 | sapi_windows_set_ctrl_handler(function () use ($checker) { 75 | throw new Exception; 76 | }); 77 | } 78 | 79 | // Run 80 | try { 81 | $fixerOk = $checker->runFixer(); 82 | echo "\n\n"; 83 | $snifferOk = $checker->runSniffer(); 84 | } catch (Exception) { 85 | echo "Terminated\n"; 86 | $checker->cleanup(); 87 | exit(1); 88 | } 89 | 90 | $checker->cleanup(); 91 | 92 | if ($fixerOk && $snifferOk) { 93 | echo $dryRun ? "Code style checks passed.\n" : "Code style fixed successfully.\n"; 94 | exit(0); 95 | } else { 96 | echo $dryRun ? "Code style issues found.\n" : "Code style fixing failed or issues remain.\n"; 97 | exit(1); 98 | } 99 | -------------------------------------------------------------------------------- /src/Checker.php: -------------------------------------------------------------------------------- 1 | fileListPath = dirname(__DIR__) . '/filelist.tmp'; 20 | } 21 | 22 | 23 | public function setPaths(array $paths): void 24 | { 25 | $finder = PhpCsFixer\Finder::create() 26 | ->name(['*.php', '*.phpt']) 27 | ->notPath([ 28 | '/fixtures.*/', 29 | 'expected', 30 | 'temp', 31 | 'tmp', 32 | 'vendor', 33 | ]) 34 | ->filter(fn(SplFileInfo $file) => !preg_match('#@phpVersion\s+([0-9.]+)#i', file_get_contents((string) $file), $m) 35 | || version_compare(PHP_VERSION, $m[1], '>=')) 36 | ->in($paths); 37 | 38 | file_put_contents($this->fileListPath, implode("\n", iterator_to_array($finder))); 39 | } 40 | 41 | 42 | /** 43 | * Runs PHP CS Fixer. 44 | * Returns true on success, false on failure. 45 | */ 46 | public function runFixer(): bool 47 | { 48 | $fixerBin = $this->vendorDir . '/friendsofphp/php-cs-fixer/php-cs-fixer'; 49 | 50 | $presetPath = dirname(__DIR__) . '/preset-fixer'; 51 | $preset = $this->preset; 52 | if ($preset === null) { 53 | $preset = $this->derivePresetFromVersion($presetPath); 54 | echo "Preset: $preset detected from PHP version\n"; 55 | } 56 | $presetFile = "$presetPath/$preset.php"; 57 | if (!is_file($presetFile)) { 58 | fwrite(STDERR, "Error: Preset configuration not found for PHP CS Fixer: {$presetFile}\n"); 59 | return false; 60 | } 61 | 62 | passthru( 63 | PHP_BINARY . ' ' . escapeshellarg($fixerBin) 64 | . ' fix -v' 65 | . ($this->dryRun ? ' --dry-run' : '') 66 | . ' --config=' . escapeshellarg($presetFile), 67 | $exitCode, 68 | ); 69 | return $exitCode === 0; 70 | } 71 | 72 | 73 | /** 74 | * Runs PHP_CodeSniffer (phpcs or phpcbf). 75 | */ 76 | public function runSniffer(): bool 77 | { 78 | $snifferBin = $this->vendorDir . '/squizlabs/php_codesniffer/bin/' . ($this->dryRun ? 'phpcs' : 'phpcbf'); 79 | 80 | $presetPath = dirname(__DIR__) . '/preset-sniffer'; 81 | $preset = $this->preset; 82 | if ($preset === null) { 83 | $preset = $this->derivePresetFromVersion($presetPath); 84 | echo "Preset: $preset detected from PHP version\n"; 85 | } 86 | $presetFile = "$presetPath/$preset.xml"; 87 | if (!is_file($presetFile)) { 88 | fwrite(STDERR, "Error: Preset ruleset not found for PHP_CodeSniffer: {$presetFile}\n"); 89 | return false; 90 | } 91 | 92 | $phpVersionOption = ''; 93 | $originalNcsContent = null; 94 | $ncsPath = $this->projectDir . '/ncs.xml'; 95 | 96 | try { 97 | if (preg_match('~php(\d)(\d)~', $preset, $m)) { 98 | $phpVersionOption = " --runtime-set php_version {$m[1]}0{$m[2]}00"; 99 | if (is_file($ncsPath)) { 100 | echo "Using custom ruleset: $ncsPath\n"; 101 | $presetFile = $ncsPath; 102 | $originalNcsContent = file_get_contents($ncsPath); 103 | file_put_contents($ncsPath, str_replace('ref="$presets/', "ref=\"$presetPath/", $originalNcsContent)); 104 | } 105 | } 106 | 107 | passthru( 108 | PHP_BINARY . ' ' . escapeshellarg($snifferBin) 109 | . ' -s' // show sniff codes, works only in dry mode :-( 110 | . ' -p' // progress 111 | . $phpVersionOption 112 | . ' --colors' 113 | . ' --extensions=php,phpt' 114 | . ' --runtime-set ignore_warnings_on_exit true' 115 | . ' --no-cache' 116 | . ' --parallel=10' 117 | . ' --standard=' . escapeshellarg($presetFile) 118 | . ' --file-list=' . escapeshellarg($this->fileListPath), 119 | $exitCode, 120 | ); 121 | 122 | } finally { 123 | if ($originalNcsContent !== null) { 124 | file_put_contents($ncsPath, $originalNcsContent); 125 | } 126 | } 127 | 128 | // phpcs returns 0 for no errors, 1 for errors found, 2 for fixable errors found (with --report=...), 3 for processing errors 129 | // phpcbf returns 0 for no errors, 1 for errors fixed, 2 for errors remaining, 3 for processing errors 130 | return $this->dryRun ? $exitCode === 0 : ($exitCode === 0 || $exitCode === 1); 131 | } 132 | 133 | 134 | /** 135 | * Derives a preset name (e.g., 'php81') from a PHP version. 136 | */ 137 | private function derivePresetFromVersion(string $path): string 138 | { 139 | $phpVersion = $this->detectPhpVersion(); 140 | $versions = array_map( 141 | fn($file) => preg_match('/php(\d)(\d+)\.\w+$/', $file, $m) ? "$m[1].$m[2]" : null, 142 | glob("$path/php*"), 143 | ); 144 | usort($versions, fn($a, $b) => -version_compare($a, $b)); 145 | foreach ($versions as $version) { 146 | if (version_compare($version, $phpVersion ?? '0', '<=')) { 147 | break; 148 | } 149 | } 150 | return 'php' . str_replace('.', '', $version); 151 | } 152 | 153 | 154 | /** 155 | * Tries to detect the required PHP version from composer.json. 156 | */ 157 | private function detectPhpVersion(): ?string 158 | { 159 | $composerPath = $this->projectDir . '/composer.json'; 160 | if (is_file($composerPath)) { 161 | $json = @json_decode(file_get_contents($composerPath)); 162 | if (preg_match('#(\d+\.\d+)#', $json->require->php ?? '', $m)) { 163 | return $m[1]; 164 | } 165 | } 166 | return null; 167 | } 168 | 169 | 170 | /** 171 | * Cleans up temporary files. 172 | */ 173 | public function cleanup(): void 174 | { 175 | @unlink($this->fileListPath); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Fixer/BracesPositionFixer.php: -------------------------------------------------------------------------------- 1 | 9 | * Dariusz Rumiński 10 | * 11 | * This source file is subject to the MIT license that is bundled 12 | * with this source code in the file LICENSE. 13 | */ 14 | 15 | // PhpCsFixer\Fixer\Basic; 16 | namespace NetteCodingStandard\Fixer\Basic; 17 | 18 | use PhpCsFixer\AbstractFixer; 19 | use PhpCsFixer\Fixer\ConfigurableFixerInterface; 20 | use PhpCsFixer\Fixer\ConfigurableFixerTrait; 21 | use PhpCsFixer\Fixer\Indentation; 22 | use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface; 23 | use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; 24 | use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; 25 | use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; 26 | use PhpCsFixer\FixerDefinition\CodeSample; 27 | use PhpCsFixer\FixerDefinition\FixerDefinition; 28 | use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; 29 | use PhpCsFixer\Preg; 30 | use PhpCsFixer\Tokenizer\CT; 31 | use PhpCsFixer\Tokenizer\Token; 32 | use PhpCsFixer\Tokenizer\Tokens; 33 | use PhpCsFixer\Tokenizer\TokensAnalyzer; 34 | 35 | /** 36 | * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> 37 | * 38 | * @phpstan-type _AutogeneratedInputConfiguration array{ 39 | * allow_single_line_anonymous_functions?: bool, 40 | * allow_single_line_empty_anonymous_classes?: bool, 41 | * anonymous_classes_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line', 42 | * anonymous_functions_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line', 43 | * classes_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line', 44 | * control_structures_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line', 45 | * functions_opening_brace?: 'next_line_unless_newline_at_signature_end'|'same_line' 46 | * } 47 | * @phpstan-type _AutogeneratedComputedConfiguration array{ 48 | * allow_single_line_anonymous_functions: bool, 49 | * allow_single_line_empty_anonymous_classes: bool, 50 | * anonymous_classes_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line', 51 | * anonymous_functions_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line', 52 | * classes_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line', 53 | * control_structures_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line', 54 | * functions_opening_brace: 'next_line_unless_newline_at_signature_end'|'same_line' 55 | * } 56 | */ 57 | final class BracesPositionFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface 58 | { 59 | /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */ 60 | use ConfigurableFixerTrait; 61 | 62 | use Indentation; 63 | 64 | /** 65 | * @internal 66 | */ 67 | public const NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END = 'next_line_unless_newline_at_signature_end'; 68 | 69 | /** 70 | * @internal 71 | */ 72 | public const SAME_LINE = 'same_line'; 73 | 74 | public function getDefinition(): FixerDefinitionInterface 75 | { 76 | return new FixerDefinition( 77 | 'Braces must be placed as configured.', 78 | [ 79 | new CodeSample( 80 | ' self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END] 108 | ), 109 | new CodeSample( 110 | ' self::SAME_LINE] 116 | ), 117 | new CodeSample( 118 | ' self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END] 123 | ), 124 | new CodeSample( 125 | ' self::SAME_LINE] 131 | ), 132 | new CodeSample( 133 | ' self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END] 138 | ), 139 | new CodeSample( 140 | ' true] 145 | ), 146 | new CodeSample( 147 | ' true] 153 | ), 154 | ] 155 | ); 156 | } 157 | 158 | public function isCandidate(Tokens $tokens): bool 159 | { 160 | return $tokens->isTokenKindFound('{'); 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | * 166 | * Must run before SingleLineEmptyBodyFixer, StatementIndentationFixer. 167 | * Must run after ControlStructureBracesFixer, NoMultipleStatementsPerLineFixer. 168 | */ 169 | public function getPriority(): int 170 | { 171 | return -2; 172 | } 173 | 174 | /** @protected */ 175 | public function createConfigurationDefinition(): FixerConfigurationResolverInterface 176 | { 177 | return new FixerConfigurationResolver([ 178 | (new FixerOptionBuilder('control_structures_opening_brace', 'The position of the opening brace of control structures‘ body.')) 179 | ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE]) 180 | ->setDefault(self::SAME_LINE) 181 | ->getOption(), 182 | (new FixerOptionBuilder('functions_opening_brace', 'The position of the opening brace of functions‘ body.')) 183 | ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE]) 184 | ->setDefault(self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END) 185 | ->getOption(), 186 | (new FixerOptionBuilder('anonymous_functions_opening_brace', 'The position of the opening brace of anonymous functions‘ body.')) 187 | ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE]) 188 | ->setDefault(self::SAME_LINE) 189 | ->getOption(), 190 | (new FixerOptionBuilder('classes_opening_brace', 'The position of the opening brace of classes‘ body.')) 191 | ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE]) 192 | ->setDefault(self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END) 193 | ->getOption(), 194 | (new FixerOptionBuilder('anonymous_classes_opening_brace', 'The position of the opening brace of anonymous classes‘ body.')) 195 | ->setAllowedValues([self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END, self::SAME_LINE]) 196 | ->setDefault(self::SAME_LINE) 197 | ->getOption(), 198 | (new FixerOptionBuilder('allow_single_line_empty_anonymous_classes', 'Allow anonymous classes to have opening and closing braces on the same line.')) 199 | ->setAllowedTypes(['bool']) 200 | ->setDefault(true) 201 | ->getOption(), 202 | (new FixerOptionBuilder('allow_single_line_anonymous_functions', 'Allow anonymous functions to have opening and closing braces on the same line.')) 203 | ->setAllowedTypes(['bool']) 204 | ->setDefault(true) 205 | ->getOption(), 206 | ]); 207 | } 208 | 209 | protected function applyFix(\SplFileInfo $file, Tokens $tokens): void 210 | { 211 | $classyTokens = Token::getClassyTokenKinds(); 212 | $controlStructureTokens = [T_DECLARE, T_DO, T_ELSE, T_ELSEIF, T_FINALLY, T_FOR, T_FOREACH, T_IF, T_WHILE, T_TRY, T_CATCH, T_SWITCH]; 213 | // @TODO: drop condition when PHP 8.0+ is required 214 | if (\defined('T_MATCH')) { 215 | $controlStructureTokens[] = T_MATCH; 216 | } 217 | 218 | $tokensAnalyzer = new TokensAnalyzer($tokens); 219 | 220 | $allowSingleLineUntil = null; 221 | 222 | foreach ($tokens as $index => $token) { 223 | $allowSingleLine = false; 224 | $allowSingleLineIfEmpty = false; 225 | 226 | if ($token->isGivenKind($classyTokens)) { 227 | $openBraceIndex = $tokens->getNextTokenOfKind($index, ['{']); 228 | 229 | if ($tokensAnalyzer->isAnonymousClass($index)) { 230 | $allowSingleLineIfEmpty = true === $this->configuration['allow_single_line_empty_anonymous_classes']; 231 | $positionOption = 'anonymous_classes_opening_brace'; 232 | } else { 233 | $positionOption = 'classes_opening_brace'; 234 | } 235 | } elseif ($token->isGivenKind(T_FUNCTION)) { 236 | $openBraceIndex = $tokens->getNextTokenOfKind($index, ['{', ';']); 237 | 238 | if ($tokens[$openBraceIndex]->equals(';')) { 239 | continue; 240 | } 241 | 242 | if ($tokensAnalyzer->isLambda($index)) { 243 | $allowSingleLine = true === $this->configuration['allow_single_line_anonymous_functions']; 244 | $positionOption = 'anonymous_functions_opening_brace'; 245 | } else { 246 | $positionOption = 'functions_opening_brace'; 247 | } 248 | } elseif ($token->isGivenKind($controlStructureTokens)) { 249 | $parenthesisEndIndex = $this->findParenthesisEnd($tokens, $index); 250 | $openBraceIndex = $tokens->getNextMeaningfulToken($parenthesisEndIndex); 251 | 252 | if (!$tokens[$openBraceIndex]->equals('{')) { 253 | continue; 254 | } 255 | 256 | $positionOption = 'control_structures_opening_brace'; 257 | } else { 258 | continue; 259 | } 260 | 261 | $closeBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openBraceIndex); 262 | 263 | $addNewlinesInsideBraces = true; 264 | if ($allowSingleLine || $allowSingleLineIfEmpty || $index < $allowSingleLineUntil) { 265 | $addNewlinesInsideBraces = false; 266 | 267 | for ($indexInsideBraces = $openBraceIndex + 1; $indexInsideBraces < $closeBraceIndex; ++$indexInsideBraces) { 268 | $tokenInsideBraces = $tokens[$indexInsideBraces]; 269 | 270 | if ( 271 | ($allowSingleLineIfEmpty && !$tokenInsideBraces->isWhitespace() && !$tokenInsideBraces->isComment()) 272 | || ($tokenInsideBraces->isWhitespace() && Preg::match('/\R/', $tokenInsideBraces->getContent())) 273 | ) { 274 | $addNewlinesInsideBraces = true; 275 | 276 | break; 277 | } 278 | } 279 | 280 | if (!$addNewlinesInsideBraces && null === $allowSingleLineUntil) { 281 | $allowSingleLineUntil = $closeBraceIndex; 282 | } 283 | } 284 | 285 | if ( 286 | $addNewlinesInsideBraces 287 | && !$this->isFollowedByNewLine($tokens, $openBraceIndex) 288 | && !$this->hasCommentOnSameLine($tokens, $openBraceIndex) 289 | && !$tokens[$tokens->getNextMeaningfulToken($openBraceIndex)]->isGivenKind(T_CLOSE_TAG) 290 | ) { 291 | $whitespace = $this->whitespacesConfig->getLineEnding().$this->getLineIndentation($tokens, $openBraceIndex); 292 | if ($tokens->ensureWhitespaceAtIndex($openBraceIndex + 1, 0, $whitespace)) { 293 | ++$closeBraceIndex; 294 | } 295 | } 296 | 297 | $whitespace = ' '; 298 | if (self::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END === $this->configuration[$positionOption]) { 299 | $whitespace = $this->whitespacesConfig->getLineEnding().$this->getLineIndentation($tokens, $index); 300 | 301 | $colon = false; 302 | $previousTokenIndex = $openBraceIndex; 303 | do { 304 | $previousTokenIndex = $tokens->getPrevMeaningfulToken($previousTokenIndex); 305 | $colon = $colon || $tokens[$previousTokenIndex]->isGivenKind([CT::T_TYPE_COLON]); 306 | } while ($tokens[$previousTokenIndex]->isGivenKind([CT::T_TYPE_COLON, CT::T_NULLABLE_TYPE, T_STRING, T_NS_SEPARATOR, CT::T_ARRAY_TYPEHINT, T_STATIC, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, T_CALLABLE, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE])); 307 | 308 | if (!$colon && $tokens[$previousTokenIndex]->equals(')')) { 309 | if ($tokens[--$previousTokenIndex]->isComment()) { 310 | --$previousTokenIndex; 311 | } 312 | if ( 313 | $tokens[$previousTokenIndex]->isWhitespace() 314 | && Preg::match('/\R/', $tokens[$previousTokenIndex]->getContent()) 315 | ) { 316 | $whitespace = ' '; 317 | } 318 | } 319 | } 320 | 321 | $moveBraceToIndex = null; 322 | 323 | if (' ' === $whitespace) { 324 | $previousMeaningfulIndex = $tokens->getPrevMeaningfulToken($openBraceIndex); 325 | for ($indexBeforeOpenBrace = $openBraceIndex - 1; $indexBeforeOpenBrace > $previousMeaningfulIndex; --$indexBeforeOpenBrace) { 326 | if (!$tokens[$indexBeforeOpenBrace]->isComment()) { 327 | continue; 328 | } 329 | 330 | $tokenBeforeOpenBrace = $tokens[--$indexBeforeOpenBrace]; 331 | if ($tokenBeforeOpenBrace->isWhitespace()) { 332 | $moveBraceToIndex = $indexBeforeOpenBrace; 333 | } elseif ($indexBeforeOpenBrace === $previousMeaningfulIndex) { 334 | $moveBraceToIndex = $previousMeaningfulIndex + 1; 335 | } 336 | } 337 | } elseif (!$tokens[$openBraceIndex - 1]->isWhitespace() || !Preg::match('/\R/', $tokens[$openBraceIndex - 1]->getContent())) { 338 | for ($indexAfterOpenBrace = $openBraceIndex + 1; $indexAfterOpenBrace < $closeBraceIndex; ++$indexAfterOpenBrace) { 339 | if ($tokens[$indexAfterOpenBrace]->isWhitespace() && Preg::match('/\R/', $tokens[$indexAfterOpenBrace]->getContent())) { 340 | break; 341 | } 342 | 343 | if ($tokens[$indexAfterOpenBrace]->isComment() && !str_starts_with($tokens[$indexAfterOpenBrace]->getContent(), '/*')) { 344 | $moveBraceToIndex = $indexAfterOpenBrace + 1; 345 | } 346 | } 347 | } 348 | 349 | if (null !== $moveBraceToIndex) { 350 | /** @var Token $movedToken */ 351 | $movedToken = clone $tokens[$openBraceIndex]; 352 | 353 | $delta = $openBraceIndex < $moveBraceToIndex ? 1 : -1; 354 | 355 | if ($tokens[$openBraceIndex + $delta]->isWhitespace()) { 356 | if (-1 === $delta && Preg::match('/\R/', $tokens[$openBraceIndex - 1]->getContent())) { 357 | $content = Preg::replace('/^(\h*?\R)?\h*/', '', $tokens[$openBraceIndex + 1]->getContent()); 358 | if ('' !== $content) { 359 | $tokens[$openBraceIndex + 1] = new Token([T_WHITESPACE, $content]); 360 | } else { 361 | $tokens->clearAt($openBraceIndex + 1); 362 | } 363 | } elseif ($tokens[$openBraceIndex - 1]->isWhitespace()) { 364 | $tokens->clearAt($openBraceIndex - 1); 365 | } 366 | } 367 | 368 | for (; $openBraceIndex !== $moveBraceToIndex; $openBraceIndex += $delta) { 369 | /** @var Token $siblingToken */ 370 | $siblingToken = $tokens[$openBraceIndex + $delta]; 371 | $tokens[$openBraceIndex] = $siblingToken; 372 | } 373 | 374 | $tokens[$openBraceIndex] = $movedToken; 375 | 376 | $openBraceIndex = $moveBraceToIndex; 377 | } 378 | 379 | if ($tokens->ensureWhitespaceAtIndex($openBraceIndex - 1, 1, $whitespace)) { 380 | ++$closeBraceIndex; 381 | if (null !== $allowSingleLineUntil) { 382 | ++$allowSingleLineUntil; 383 | } 384 | } 385 | 386 | if ( 387 | !$addNewlinesInsideBraces 388 | || $tokens[$tokens->getPrevMeaningfulToken($closeBraceIndex)]->isGivenKind(T_OPEN_TAG) 389 | ) { 390 | continue; 391 | } 392 | 393 | $prevIndex = $closeBraceIndex - 1; 394 | while ($tokens->isEmptyAt($prevIndex)) { 395 | --$prevIndex; 396 | } 397 | 398 | $prevToken = $tokens[$prevIndex]; 399 | 400 | if ($prevToken->isWhitespace() && Preg::match('/\R/', $prevToken->getContent())) { 401 | continue; 402 | } 403 | 404 | $whitespace = $this->whitespacesConfig->getLineEnding().$this->getLineIndentation($tokens, $openBraceIndex); 405 | $tokens->ensureWhitespaceAtIndex($prevIndex, 1, $whitespace); 406 | } 407 | } 408 | 409 | private function findParenthesisEnd(Tokens $tokens, int $structureTokenIndex): int 410 | { 411 | $nextIndex = $tokens->getNextMeaningfulToken($structureTokenIndex); 412 | $nextToken = $tokens[$nextIndex]; 413 | 414 | // return if next token is not opening parenthesis 415 | if (!$nextToken->equals('(')) { 416 | return $structureTokenIndex; 417 | } 418 | 419 | return $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextIndex); 420 | } 421 | 422 | private function isFollowedByNewLine(Tokens $tokens, int $index): bool 423 | { 424 | for (++$index, $max = \count($tokens) - 1; $index < $max; ++$index) { 425 | $token = $tokens[$index]; 426 | if (!$token->isComment()) { 427 | return $token->isWhitespace() && Preg::match('/\R/', $token->getContent()); 428 | } 429 | } 430 | 431 | return false; 432 | } 433 | 434 | private function hasCommentOnSameLine(Tokens $tokens, int $index): bool 435 | { 436 | $token = $tokens[$index + 1]; 437 | 438 | if ($token->isWhitespace() && !Preg::match('/\R/', $token->getContent())) { 439 | $token = $tokens[$index + 2]; 440 | } 441 | 442 | return $token->isComment(); 443 | } 444 | 445 | public function getName(): string 446 | { 447 | return 'Nette/' . parent::getName(); 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /src/Fixer/ClassAndTraitVisibilityRequiredFixer.php: -------------------------------------------------------------------------------- 1 | visibilityRequiredFixer = new VisibilityRequiredFixer; 25 | parent::__construct(); 26 | } 27 | 28 | 29 | public function isCandidate(Tokens $tokens): bool 30 | { 31 | return $tokens->isAnyTokenKindsFound([T_CLASS, T_TRAIT]); 32 | } 33 | 34 | 35 | public function getDefinition(): FixerDefinitionInterface 36 | { 37 | return $this->visibilityRequiredFixer->getDefinition(); 38 | } 39 | 40 | 41 | public function getPriority(): int 42 | { 43 | return $this->visibilityRequiredFixer->getPriority(); 44 | } 45 | 46 | 47 | public function configure(array $configuration): void 48 | { 49 | $this->configuration = $configuration; 50 | $this->visibilityRequiredFixer->configure($configuration); 51 | } 52 | 53 | 54 | public function getConfigurationDefinition(): FixerConfigurationResolverInterface 55 | { 56 | return $this->visibilityRequiredFixer->getConfigurationDefinition(); 57 | } 58 | 59 | 60 | protected function applyFix(SplFileInfo $file, Tokens $tokens): void 61 | { 62 | /** 63 | * Hack note: This reflection opening was chosen as more future-proof 64 | * than duplicating whole 300-lines class. As "VisibilityRequiredFixer" class is final 65 | * and "applyFix()" is final, there is no other way round it. 66 | */ 67 | $method = new ReflectionMethod($this->visibilityRequiredFixer, 'applyFix'); 68 | $method->setAccessible(true); 69 | $method->invoke($this->visibilityRequiredFixer, $file, $tokens); 70 | } 71 | 72 | 73 | public function getName(): string 74 | { 75 | return 'Nette/' . parent::getName(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Fixer/MethodArgumentSpaceFixer.php: -------------------------------------------------------------------------------- 1 | 9 | * Dariusz Rumiński 10 | * 11 | * This source file is subject to the MIT license that is bundled 12 | * with this source code in the file LICENSE. 13 | */ 14 | 15 | // PhpCsFixer\Fixer\FunctionNotation 16 | namespace NetteCodingStandard\Fixer\FunctionNotation; 17 | 18 | use PhpCsFixer\AbstractFixer; 19 | use PhpCsFixer\Fixer\ConfigurableFixerInterface; 20 | use PhpCsFixer\Fixer\ConfigurableFixerTrait; 21 | use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface; 22 | use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; 23 | use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; 24 | use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; 25 | use PhpCsFixer\FixerDefinition\CodeSample; 26 | use PhpCsFixer\FixerDefinition\FixerDefinition; 27 | use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; 28 | use PhpCsFixer\Preg; 29 | use PhpCsFixer\Tokenizer\CT; 30 | use PhpCsFixer\Tokenizer\Token; 31 | use PhpCsFixer\Tokenizer\Tokens; 32 | 33 | /** 34 | * @author Kuanhung Chen 35 | * 36 | * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> 37 | * 38 | * @phpstan-type _AutogeneratedInputConfiguration array{ 39 | * after_heredoc?: bool, 40 | * attribute_placement?: 'ignore'|'same_line'|'standalone', 41 | * keep_multiple_spaces_after_comma?: bool, 42 | * on_multiline?: 'ensure_fully_multiline'|'ensure_single_line'|'ignore' 43 | * } 44 | * @phpstan-type _AutogeneratedComputedConfiguration array{ 45 | * after_heredoc: bool, 46 | * attribute_placement: 'ignore'|'same_line'|'standalone', 47 | * keep_multiple_spaces_after_comma: bool, 48 | * on_multiline: 'ensure_fully_multiline'|'ensure_single_line'|'ignore' 49 | * } 50 | */ 51 | final class MethodArgumentSpaceFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface 52 | { 53 | /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */ 54 | use ConfigurableFixerTrait; 55 | 56 | public function getDefinition(): FixerDefinitionInterface 57 | { 58 | return new FixerDefinition( 59 | 'In method arguments and method call, there MUST NOT be a space before each comma and there MUST be one space after each comma. Argument lists MAY be split across multiple lines, where each subsequent line is indented once. When doing so, the first item in the list MUST be on the next line, and there MUST be only one argument per line.', 60 | [ 61 | new CodeSample( 62 | " false] 68 | ), 69 | new CodeSample( 70 | " true] 72 | ), 73 | new CodeSample( 74 | " 'ensure_fully_multiline'] 76 | ), 77 | new CodeSample( 78 | " 'ensure_single_line'] 80 | ), 81 | new CodeSample( 82 | " 'ensure_fully_multiline', 85 | 'keep_multiple_spaces_after_comma' => true, 86 | ] 87 | ), 88 | new CodeSample( 89 | " 'ensure_fully_multiline', 92 | 'keep_multiple_spaces_after_comma' => false, 93 | ] 94 | ), 95 | new CodeSample( 96 | " 'ensure_fully_multiline', 99 | 'attribute_placement' => 'ignore', 100 | ] 101 | ), 102 | new CodeSample( 103 | " 'ensure_fully_multiline', 106 | 'attribute_placement' => 'same_line', 107 | ] 108 | ), 109 | new CodeSample( 110 | " 'ensure_fully_multiline', 113 | 'attribute_placement' => 'standalone', 114 | ] 115 | ), 116 | new CodeSample( 117 | <<<'SAMPLE' 118 | true] 130 | ), 131 | ], 132 | 'This fixer covers rules defined in PSR2 ¶4.4, ¶4.6.' 133 | ); 134 | } 135 | 136 | public function isCandidate(Tokens $tokens): bool 137 | { 138 | return $tokens->isTokenKindFound('('); 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | * 144 | * Must run before ArrayIndentationFixer, StatementIndentationFixer. 145 | * Must run after CombineNestedDirnameFixer, FunctionDeclarationFixer, ImplodeCallFixer, LambdaNotUsedImportFixer, NoMultilineWhitespaceAroundDoubleArrowFixer, NoUselessSprintfFixer, PowToExponentiationFixer, StrictParamFixer. 146 | */ 147 | public function getPriority(): int 148 | { 149 | return 30; 150 | } 151 | 152 | protected function applyFix(\SplFileInfo $file, Tokens $tokens): void 153 | { 154 | $expectedTokens = [T_LIST, T_FUNCTION, CT::T_USE_LAMBDA, T_FN, T_CLASS]; 155 | 156 | for ($index = $tokens->count() - 1; $index > 0; --$index) { 157 | $token = $tokens[$index]; 158 | 159 | if (!$token->equals('(')) { 160 | continue; 161 | } 162 | 163 | $meaningfulTokenBeforeParenthesis = $tokens[$tokens->getPrevMeaningfulToken($index)]; 164 | 165 | if ($meaningfulTokenBeforeParenthesis->isGivenKind(T_STRING)) { 166 | $isMultiline = $this->fixFunction($tokens, $index); 167 | 168 | if ( 169 | $isMultiline 170 | && 'ensure_fully_multiline' === $this->configuration['on_multiline'] 171 | && !$meaningfulTokenBeforeParenthesis->isGivenKind(T_LIST) 172 | ) { 173 | $this->ensureFunctionFullyMultiline($tokens, $index); 174 | } 175 | } 176 | } 177 | } 178 | 179 | protected function createConfigurationDefinition(): FixerConfigurationResolverInterface 180 | { 181 | return new FixerConfigurationResolver([ 182 | (new FixerOptionBuilder('keep_multiple_spaces_after_comma', 'Whether keep multiple spaces after comma.')) 183 | ->setAllowedTypes(['bool']) 184 | ->setDefault(false) 185 | ->getOption(), 186 | (new FixerOptionBuilder( 187 | 'on_multiline', 188 | 'Defines how to handle function arguments lists that contain newlines.' 189 | )) 190 | ->setAllowedValues(['ignore', 'ensure_single_line', 'ensure_fully_multiline']) 191 | ->setDefault('ensure_fully_multiline') 192 | ->getOption(), 193 | (new FixerOptionBuilder('after_heredoc', 'Whether the whitespace between heredoc end and comma should be removed.')) 194 | ->setAllowedTypes(['bool']) 195 | ->setDefault(false) 196 | ->getOption(), 197 | (new FixerOptionBuilder( 198 | 'attribute_placement', 199 | 'Defines how to handle argument attributes when function definition is multiline.' 200 | )) 201 | ->setAllowedValues(['ignore', 'same_line', 'standalone']) 202 | ->setDefault('standalone') 203 | ->getOption(), 204 | ]); 205 | } 206 | 207 | /** 208 | * Fix arguments spacing for given function. 209 | * 210 | * @param Tokens $tokens Tokens to handle 211 | * @param int $startFunctionIndex Start parenthesis position 212 | * 213 | * @return bool whether the function is multiline 214 | */ 215 | private function fixFunction(Tokens $tokens, int $startFunctionIndex): bool 216 | { 217 | $isMultiline = false; 218 | 219 | $endFunctionIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startFunctionIndex); 220 | $firstWhitespaceIndex = $this->findWhitespaceIndexAfterParenthesis($tokens, $startFunctionIndex, $endFunctionIndex); 221 | $lastWhitespaceIndex = $this->findWhitespaceIndexAfterParenthesis($tokens, $endFunctionIndex, $startFunctionIndex); 222 | 223 | foreach ([$firstWhitespaceIndex, $lastWhitespaceIndex] as $index) { 224 | if (null === $index || !Preg::match('/\R/', $tokens[$index]->getContent())) { 225 | continue; 226 | } 227 | 228 | if ('ensure_single_line' !== $this->configuration['on_multiline']) { 229 | $isMultiline = true; 230 | 231 | continue; 232 | } 233 | 234 | $newLinesRemoved = $this->ensureSingleLine($tokens, $index); 235 | 236 | if (!$newLinesRemoved) { 237 | $isMultiline = true; 238 | } 239 | } 240 | 241 | for ($index = $endFunctionIndex - 1; $index > $startFunctionIndex; --$index) { 242 | $token = $tokens[$index]; 243 | 244 | if ($token->equals(')')) { 245 | $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index); 246 | 247 | continue; 248 | } 249 | 250 | if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) { 251 | $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index); 252 | 253 | continue; 254 | } 255 | 256 | if ($token->equals('}')) { 257 | $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index); 258 | 259 | continue; 260 | } 261 | 262 | if ($token->equals(',')) { 263 | $this->fixSpace($tokens, $index); 264 | if (!$isMultiline && $this->isNewline($tokens[$index + 1])) { 265 | $isMultiline = true; 266 | } 267 | } 268 | } 269 | 270 | return $isMultiline; 271 | } 272 | 273 | private function findWhitespaceIndexAfterParenthesis(Tokens $tokens, int $startParenthesisIndex, int $endParenthesisIndex): ?int 274 | { 275 | $direction = $endParenthesisIndex > $startParenthesisIndex ? 1 : -1; 276 | $startIndex = $startParenthesisIndex + $direction; 277 | $endIndex = $endParenthesisIndex - $direction; 278 | 279 | for ($index = $startIndex; $index !== $endIndex; $index += $direction) { 280 | $token = $tokens[$index]; 281 | 282 | if ($token->isWhitespace()) { 283 | return $index; 284 | } 285 | 286 | if (!$token->isComment()) { 287 | break; 288 | } 289 | } 290 | 291 | return null; 292 | } 293 | 294 | /** 295 | * @return bool Whether newlines were removed from the whitespace token 296 | */ 297 | private function ensureSingleLine(Tokens $tokens, int $index): bool 298 | { 299 | $previousToken = $tokens[$index - 1]; 300 | 301 | if ($previousToken->isComment() && !str_starts_with($previousToken->getContent(), '/*')) { 302 | return false; 303 | } 304 | 305 | $content = Preg::replace('/\R\h*/', '', $tokens[$index]->getContent()); 306 | 307 | $tokens->ensureWhitespaceAtIndex($index, 0, $content); 308 | 309 | return true; 310 | } 311 | 312 | private function ensureFunctionFullyMultiline(Tokens $tokens, int $startFunctionIndex): void 313 | { 314 | // find out what the indentation is 315 | $searchIndex = $startFunctionIndex; 316 | do { 317 | $prevWhitespaceTokenIndex = $tokens->getPrevTokenOfKind( 318 | $searchIndex, 319 | [[T_ENCAPSED_AND_WHITESPACE], [T_WHITESPACE]], 320 | ); 321 | 322 | $searchIndex = $prevWhitespaceTokenIndex; 323 | } while (null !== $prevWhitespaceTokenIndex 324 | && !str_contains($tokens[$prevWhitespaceTokenIndex]->getContent(), "\n") 325 | ); 326 | 327 | if (null === $prevWhitespaceTokenIndex) { 328 | $existingIndentation = ''; 329 | } elseif (!$tokens[$prevWhitespaceTokenIndex]->isGivenKind(T_WHITESPACE)) { 330 | return; 331 | } else { 332 | $existingIndentation = $tokens[$prevWhitespaceTokenIndex]->getContent(); 333 | $lastLineIndex = strrpos($existingIndentation, "\n"); 334 | $existingIndentation = false === $lastLineIndex 335 | ? $existingIndentation 336 | : substr($existingIndentation, $lastLineIndex + 1); 337 | } 338 | 339 | $indentation = $existingIndentation.$this->whitespacesConfig->getIndent(); 340 | $endFunctionIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startFunctionIndex); 341 | 342 | $wasWhitespaceBeforeEndFunctionAddedAsNewToken = $tokens->ensureWhitespaceAtIndex( 343 | $tokens[$endFunctionIndex - 1]->isWhitespace() ? $endFunctionIndex - 1 : $endFunctionIndex, 344 | 0, 345 | $this->whitespacesConfig->getLineEnding().$existingIndentation 346 | ); 347 | 348 | if ($wasWhitespaceBeforeEndFunctionAddedAsNewToken) { 349 | ++$endFunctionIndex; 350 | } 351 | 352 | for ($index = $endFunctionIndex - 1; $index > $startFunctionIndex; --$index) { 353 | $token = $tokens[$index]; 354 | 355 | // skip nested method calls and arrays 356 | if ($token->equals(')')) { 357 | $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index); 358 | 359 | continue; 360 | } 361 | 362 | // skip nested arrays 363 | if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) { 364 | $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index); 365 | 366 | continue; 367 | } 368 | 369 | if ($token->equals('}')) { 370 | $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index); 371 | 372 | continue; 373 | } 374 | 375 | if ($tokens[$tokens->getNextMeaningfulToken($index)]->equals(')')) { 376 | continue; 377 | } 378 | 379 | if ($token->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) { 380 | if ('standalone' === $this->configuration['attribute_placement']) { 381 | $this->fixNewline($tokens, $index, $indentation); 382 | } elseif ('same_line' === $this->configuration['attribute_placement']) { 383 | $this->ensureSingleLine($tokens, $index + 1); 384 | $tokens->ensureWhitespaceAtIndex($index + 1, 0, ' '); 385 | } 386 | $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index); 387 | 388 | continue; 389 | } 390 | 391 | if ($token->equals(',')) { 392 | $this->fixNewline($tokens, $index, $indentation); 393 | } 394 | } 395 | 396 | $this->fixNewline($tokens, $startFunctionIndex, $indentation, false); 397 | } 398 | 399 | /** 400 | * Method to insert newline after comma, attribute or opening parenthesis. 401 | * 402 | * @param int $index index of a comma 403 | * @param string $indentation the indentation that should be used 404 | * @param bool $override whether to override the existing character or not 405 | */ 406 | private function fixNewline(Tokens $tokens, int $index, string $indentation, bool $override = true): void 407 | { 408 | if ($tokens[$index + 1]->isComment()) { 409 | return; 410 | } 411 | 412 | if ($tokens[$index + 2]->isComment()) { 413 | $nextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($index + 2); 414 | if (!$this->isNewline($tokens[$nextMeaningfulTokenIndex - 1])) { 415 | if ($tokens[$nextMeaningfulTokenIndex - 1]->isWhitespace()) { 416 | $tokens->clearAt($nextMeaningfulTokenIndex - 1); 417 | } 418 | 419 | $tokens->ensureWhitespaceAtIndex($nextMeaningfulTokenIndex, 0, $this->whitespacesConfig->getLineEnding().$indentation); 420 | } 421 | 422 | return; 423 | } 424 | 425 | $nextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($index); 426 | 427 | if ($tokens[$nextMeaningfulTokenIndex]->equals(')')) { 428 | return; 429 | } 430 | 431 | $tokens->ensureWhitespaceAtIndex($index + 1, 0, $this->whitespacesConfig->getLineEnding().$indentation); 432 | } 433 | 434 | /** 435 | * Method to insert space after comma and remove space before comma. 436 | */ 437 | private function fixSpace(Tokens $tokens, int $index): void 438 | { 439 | // remove space before comma if exist 440 | if ($tokens[$index - 1]->isWhitespace()) { 441 | $prevIndex = $tokens->getPrevNonWhitespace($index - 1); 442 | 443 | if ( 444 | !$tokens[$prevIndex]->equals(',') && !$tokens[$prevIndex]->isComment() 445 | && (true === $this->configuration['after_heredoc'] || !$tokens[$prevIndex]->isGivenKind(T_END_HEREDOC)) 446 | ) { 447 | $tokens->clearAt($index - 1); 448 | } 449 | } 450 | 451 | $nextIndex = $index + 1; 452 | $nextToken = $tokens[$nextIndex]; 453 | 454 | // Two cases for fix space after comma (exclude multiline comments) 455 | // 1) multiple spaces after comma 456 | // 2) no space after comma 457 | if ($nextToken->isWhitespace()) { 458 | $newContent = $nextToken->getContent(); 459 | 460 | if ('ensure_single_line' === $this->configuration['on_multiline']) { 461 | $newContent = Preg::replace('/\R/', '', $newContent); 462 | } 463 | 464 | if ( 465 | (false === $this->configuration['keep_multiple_spaces_after_comma'] || Preg::match('/\R/', $newContent)) 466 | && !$this->isCommentLastLineToken($tokens, $index + 2) 467 | ) { 468 | $newContent = ltrim($newContent, " \t"); 469 | } 470 | 471 | $tokens[$nextIndex] = new Token([T_WHITESPACE, '' === $newContent ? ' ' : $newContent]); 472 | 473 | return; 474 | } 475 | 476 | if (!$this->isCommentLastLineToken($tokens, $index + 1)) { 477 | $tokens->insertAt($index + 1, new Token([T_WHITESPACE, ' '])); 478 | } 479 | } 480 | 481 | /** 482 | * Check if last item of current line is a comment. 483 | * 484 | * @param Tokens $tokens tokens to handle 485 | * @param int $index index of token 486 | */ 487 | private function isCommentLastLineToken(Tokens $tokens, int $index): bool 488 | { 489 | if (!$tokens[$index]->isComment() || !$tokens[$index + 1]->isWhitespace()) { 490 | return false; 491 | } 492 | 493 | $content = $tokens[$index + 1]->getContent(); 494 | 495 | return $content !== ltrim($content, "\r\n"); 496 | } 497 | 498 | /** 499 | * Checks if token is new line. 500 | */ 501 | private function isNewline(Token $token): bool 502 | { 503 | return $token->isWhitespace() && str_contains($token->getContent(), "\n"); 504 | } 505 | 506 | public function getName(): string 507 | { 508 | return 'Nette/' . parent::getName(); 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /src/Fixer/StatementIndentationFixer.php: -------------------------------------------------------------------------------- 1 | 9 | * Dariusz Rumiński 10 | * 11 | * This source file is subject to the MIT license that is bundled 12 | * with this source code in the file LICENSE. 13 | */ 14 | 15 | // PhpCsFixer\Fixer\Whitespace 16 | namespace NetteCodingStandard\Fixer\Whitespace; 17 | 18 | use PhpCsFixer\AbstractFixer; 19 | use PhpCsFixer\Fixer\ConfigurableFixerInterface; 20 | use PhpCsFixer\Fixer\ConfigurableFixerTrait; 21 | use PhpCsFixer\Fixer\Indentation; 22 | use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface; 23 | use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; 24 | use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; 25 | use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; 26 | use PhpCsFixer\FixerDefinition\CodeSample; 27 | use PhpCsFixer\FixerDefinition\FixerDefinition; 28 | use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; 29 | use PhpCsFixer\Preg; 30 | use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer; 31 | use PhpCsFixer\Tokenizer\CT; 32 | use PhpCsFixer\Tokenizer\Token; 33 | use PhpCsFixer\Tokenizer\Tokens; 34 | 35 | /** 36 | * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> 37 | * 38 | * @phpstan-type _AutogeneratedInputConfiguration array{ 39 | * stick_comment_to_next_continuous_control_statement?: bool, 40 | * } 41 | * @phpstan-type _AutogeneratedComputedConfiguration array{ 42 | * stick_comment_to_next_continuous_control_statement: bool, 43 | * } 44 | */ 45 | final class StatementIndentationFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface 46 | { 47 | /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */ 48 | use ConfigurableFixerTrait; 49 | 50 | use Indentation; 51 | 52 | private AlternativeSyntaxAnalyzer $alternativeSyntaxAnalyzer; 53 | 54 | private bool $bracesFixerCompatibility; 55 | 56 | public function __construct(bool $bracesFixerCompatibility = false) 57 | { 58 | parent::__construct(); 59 | 60 | $this->bracesFixerCompatibility = $bracesFixerCompatibility; 61 | } 62 | 63 | public function getDefinition(): FixerDefinitionInterface 64 | { 65 | return new FixerDefinition( 66 | 'Each statement must be indented.', 67 | [ 68 | new CodeSample( 69 | ' false] 89 | ), 90 | new CodeSample( 91 | ' true] 106 | ), 107 | ] 108 | ); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | * 114 | * Must run before HeredocIndentationFixer. 115 | * Must run after BracesPositionFixer, ClassAttributesSeparationFixer, CurlyBracesPositionFixer, FullyQualifiedStrictTypesFixer, GlobalNamespaceImportFixer, MethodArgumentSpaceFixer, NoUselessElseFixer, YieldFromArrayToYieldsFixer. 116 | */ 117 | public function getPriority(): int 118 | { 119 | return -3; 120 | } 121 | 122 | public function isCandidate(Tokens $tokens): bool 123 | { 124 | return true; 125 | } 126 | 127 | protected function createConfigurationDefinition(): FixerConfigurationResolverInterface 128 | { 129 | return new FixerConfigurationResolver([ 130 | (new FixerOptionBuilder('stick_comment_to_next_continuous_control_statement', 'Last comment of code block counts as comment for next block.')) 131 | ->setAllowedTypes(['bool']) 132 | ->setDefault(false) 133 | ->getOption(), 134 | ]); 135 | } 136 | 137 | protected function applyFix(\SplFileInfo $file, Tokens $tokens): void 138 | { 139 | $this->alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer(); 140 | 141 | $blockSignatureFirstTokens = [ 142 | T_USE, 143 | T_IF, 144 | T_ELSE, 145 | T_ELSEIF, 146 | T_FOR, 147 | T_FOREACH, 148 | T_WHILE, 149 | T_DO, 150 | T_SWITCH, 151 | T_CASE, 152 | T_DEFAULT, 153 | T_TRY, 154 | T_CLASS, 155 | T_INTERFACE, 156 | T_TRAIT, 157 | T_EXTENDS, 158 | T_IMPLEMENTS, 159 | T_CONST, 160 | ]; 161 | $controlStructurePossibiblyWithoutBracesTokens = [ 162 | T_IF, 163 | T_ELSE, 164 | T_ELSEIF, 165 | T_FOR, 166 | T_FOREACH, 167 | T_WHILE, 168 | T_DO, 169 | ]; 170 | if (\defined('T_MATCH')) { // @TODO: drop condition when PHP 8.0+ is required 171 | $blockSignatureFirstTokens[] = T_MATCH; 172 | } 173 | 174 | $blockFirstTokens = ['{', [CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN], [CT::T_USE_TRAIT], [CT::T_GROUP_IMPORT_BRACE_OPEN], [CT::T_PROPERTY_HOOK_BRACE_OPEN]]; 175 | if (\defined('T_ATTRIBUTE')) { // @TODO: drop condition when PHP 8.0+ is required 176 | $blockFirstTokens[] = [T_ATTRIBUTE]; 177 | } 178 | 179 | $endIndex = \count($tokens) - 1; 180 | if ($tokens[$endIndex]->isWhitespace()) { 181 | --$endIndex; 182 | } 183 | 184 | $lastIndent = $this->getLineIndentationWithBracesCompatibility( 185 | $tokens, 186 | 0, 187 | $this->extractIndent($this->computeNewLineContent($tokens, 0)), 188 | ); 189 | 190 | /** 191 | * @var list $scopes 200 | */ 201 | $scopes = [ 202 | [ 203 | 'type' => 'block', 204 | 'skip' => false, 205 | 'end_index' => $endIndex, 206 | 'end_index_inclusive' => true, 207 | 'initial_indent' => $lastIndent, 208 | 'is_indented_block' => false, 209 | ], 210 | ]; 211 | 212 | $previousLineInitialIndent = ''; 213 | $previousLineNewIndent = ''; 214 | $noBracesBlockStarts = []; 215 | $alternativeBlockStarts = []; 216 | $caseBlockStarts = []; 217 | 218 | foreach ($tokens as $index => $token) { 219 | $currentScope = \count($scopes) - 1; 220 | 221 | if (isset($noBracesBlockStarts[$index])) { 222 | $scopes[] = [ 223 | 'type' => 'block', 224 | 'skip' => false, 225 | 'end_index' => $this->findStatementEndIndex($tokens, $index, \count($tokens) - 1) + 1, 226 | 'end_index_inclusive' => true, 227 | 'initial_indent' => $this->getLineIndentationWithBracesCompatibility($tokens, $index, $lastIndent), 228 | 'is_indented_block' => true, 229 | ]; 230 | ++$currentScope; 231 | } 232 | 233 | if ( 234 | $token->equalsAny($blockFirstTokens) 235 | || ($token->equals('(') && !$tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(T_ARRAY)) 236 | || isset($alternativeBlockStarts[$index]) 237 | || isset($caseBlockStarts[$index]) 238 | ) { 239 | $endIndexInclusive = true; 240 | 241 | if ($token->isGivenKind([T_EXTENDS, T_IMPLEMENTS])) { 242 | $endIndex = $tokens->getNextTokenOfKind($index, ['{']); 243 | } elseif ($token->isGivenKind(CT::T_USE_TRAIT)) { 244 | $endIndex = $tokens->getNextTokenOfKind($index, [';']); 245 | } elseif ($token->equals(':')) { 246 | if (isset($caseBlockStarts[$index])) { 247 | [$endIndex, $endIndexInclusive] = $this->findCaseBlockEnd($tokens, $index); 248 | } elseif ($this->alternativeSyntaxAnalyzer->belongsToAlternativeSyntax($tokens, $index)) { 249 | $endIndex = $this->alternativeSyntaxAnalyzer->findAlternativeSyntaxBlockEnd($tokens, $alternativeBlockStarts[$index]); 250 | } 251 | } elseif ($token->isGivenKind(CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN)) { 252 | $endIndex = $tokens->getNextTokenOfKind($index, [[CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE]]); 253 | } elseif ($token->isGivenKind(CT::T_GROUP_IMPORT_BRACE_OPEN)) { 254 | $endIndex = $tokens->getNextTokenOfKind($index, [[CT::T_GROUP_IMPORT_BRACE_CLOSE]]); 255 | } elseif ($token->equals('{')) { 256 | $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index); 257 | } elseif ($token->equals('(')) { 258 | $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index); 259 | } elseif ($token->isGivenKind(CT::T_PROPERTY_HOOK_BRACE_OPEN)) { 260 | $endIndex = $tokens->getNextTokenOfKind($index, [[CT::T_PROPERTY_HOOK_BRACE_CLOSE]]); 261 | } else { 262 | $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $index); 263 | } 264 | 265 | if ('block_signature' === $scopes[$currentScope]['type']) { 266 | $initialIndent = $scopes[$currentScope]['initial_indent']; 267 | } else { 268 | $initialIndent = $this->getLineIndentationWithBracesCompatibility($tokens, $index, $lastIndent); 269 | } 270 | 271 | $skip = false; 272 | if ($this->bracesFixerCompatibility) { 273 | $prevIndex = $tokens->getPrevMeaningfulToken($index); 274 | if (null !== $prevIndex) { 275 | $prevIndex = $tokens->getPrevMeaningfulToken($prevIndex); 276 | } 277 | if (null !== $prevIndex && $tokens[$prevIndex]->isGivenKind([T_FUNCTION, T_FN])) { 278 | $skip = true; 279 | } 280 | } 281 | 282 | $scopes[] = [ 283 | 'type' => 'block', 284 | 'skip' => $skip, 285 | 'end_index' => $endIndex, 286 | 'end_index_inclusive' => $endIndexInclusive, 287 | 'initial_indent' => $initialIndent, 288 | 'is_indented_block' => true, 289 | ]; 290 | ++$currentScope; 291 | 292 | while ($index >= $scopes[$currentScope]['end_index']) { 293 | array_pop($scopes); 294 | 295 | --$currentScope; 296 | } 297 | 298 | continue; 299 | } 300 | 301 | if ( 302 | $token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN) 303 | || ($token->equals('(') && $tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(T_ARRAY)) 304 | ) { 305 | $blockType = $token->equals('(') ? Tokens::BLOCK_TYPE_PARENTHESIS_BRACE : Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE; 306 | 307 | $scopes[] = [ 308 | 'type' => 'statement', 309 | 'skip' => true, 310 | 'end_index' => $tokens->findBlockEnd($blockType, $index), 311 | 'end_index_inclusive' => true, 312 | 'initial_indent' => $previousLineInitialIndent, 313 | 'new_indent' => $previousLineNewIndent, 314 | 'is_indented_block' => false, 315 | ]; 316 | 317 | continue; 318 | } 319 | 320 | $isPropertyStart = $this->isPropertyStart($tokens, $index); 321 | if ($isPropertyStart || $token->isGivenKind($blockSignatureFirstTokens)) { 322 | $lastWhitespaceIndex = null; 323 | $closingParenthesisIndex = null; 324 | 325 | for ($endIndex = $index + 1, $max = \count($tokens); $endIndex < $max; ++$endIndex) { 326 | $endToken = $tokens[$endIndex]; 327 | 328 | if ($endToken->equals('(')) { 329 | $closingParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $endIndex); 330 | $endIndex = $closingParenthesisIndex; 331 | 332 | continue; 333 | } 334 | 335 | if ($endToken->equalsAny(['{', ';', [T_DOUBLE_ARROW], [T_IMPLEMENTS]])) { 336 | break; 337 | } 338 | 339 | if ($endToken->equals(':')) { 340 | if ($token->isGivenKind([T_CASE, T_DEFAULT])) { 341 | $caseBlockStarts[$endIndex] = $index; 342 | } else { 343 | $alternativeBlockStarts[$endIndex] = $index; 344 | } 345 | 346 | break; 347 | } 348 | 349 | if (!$token->isGivenKind($controlStructurePossibiblyWithoutBracesTokens)) { 350 | continue; 351 | } 352 | 353 | if ($endToken->isWhitespace()) { 354 | $lastWhitespaceIndex = $endIndex; 355 | 356 | continue; 357 | } 358 | 359 | if (!$endToken->isComment()) { 360 | $noBraceBlockStartIndex = $lastWhitespaceIndex ?? $endIndex; 361 | $noBracesBlockStarts[$noBraceBlockStartIndex] = true; 362 | 363 | $endIndex = $closingParenthesisIndex ?? $index; 364 | 365 | break; 366 | } 367 | } 368 | 369 | $scopes[] = [ 370 | 'type' => 'block_signature', 371 | 'skip' => false, 372 | 'end_index' => $endIndex, 373 | 'end_index_inclusive' => true, 374 | 'initial_indent' => $this->getLineIndentationWithBracesCompatibility($tokens, $index, $lastIndent), 375 | 'is_indented_block' => $isPropertyStart || $token->isGivenKind([T_EXTENDS, T_IMPLEMENTS, T_CONST]), 376 | ]; 377 | 378 | continue; 379 | } 380 | 381 | if ($token->isGivenKind(T_FUNCTION)) { 382 | $endIndex = $index + 1; 383 | 384 | for ($max = \count($tokens); $endIndex < $max; ++$endIndex) { 385 | if ($tokens[$endIndex]->equals('(')) { 386 | $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $endIndex); 387 | 388 | continue; 389 | } 390 | 391 | if ($tokens[$endIndex]->equalsAny(['{', ';'])) { 392 | break; 393 | } 394 | } 395 | 396 | $scopes[] = [ 397 | 'type' => 'block_signature', 398 | 'skip' => false, 399 | 'end_index' => $endIndex, 400 | 'end_index_inclusive' => true, 401 | 'initial_indent' => $this->getLineIndentationWithBracesCompatibility($tokens, $index, $lastIndent), 402 | 'is_indented_block' => false, 403 | ]; 404 | 405 | continue; 406 | } 407 | 408 | if ( 409 | $token->isWhitespace() 410 | || ($index > 0 && $tokens[$index - 1]->isGivenKind(T_OPEN_TAG)) 411 | ) { 412 | $previousOpenTagContent = $tokens[$index - 1]->isGivenKind(T_OPEN_TAG) 413 | ? Preg::replace('/\S/', '', $tokens[$index - 1]->getContent()) 414 | : ''; 415 | 416 | $content = $previousOpenTagContent.($token->isWhitespace() ? $token->getContent() : ''); 417 | 418 | if (!Preg::match('/\R/', $content)) { 419 | continue; 420 | } 421 | 422 | $nextToken = $tokens[$index + 1] ?? null; 423 | 424 | if ( 425 | $this->bracesFixerCompatibility 426 | && null !== $nextToken 427 | && $nextToken->isComment() 428 | && !$this->isCommentWithFixableIndentation($tokens, $index + 1) 429 | ) { 430 | continue; 431 | } 432 | 433 | if ('block' === $scopes[$currentScope]['type'] || 'block_signature' === $scopes[$currentScope]['type']) { 434 | $indent = false; 435 | 436 | if ($scopes[$currentScope]['is_indented_block']) { 437 | $firstNonWhitespaceTokenIndex = null; 438 | $nextNewlineIndex = null; 439 | for ($searchIndex = $index + 1, $max = \count($tokens); $searchIndex < $max; ++$searchIndex) { 440 | $searchToken = $tokens[$searchIndex]; 441 | 442 | if (!$searchToken->isWhitespace()) { 443 | if (null === $firstNonWhitespaceTokenIndex) { 444 | $firstNonWhitespaceTokenIndex = $searchIndex; 445 | } 446 | 447 | continue; 448 | } 449 | 450 | if (Preg::match('/\R/', $searchToken->getContent())) { 451 | $nextNewlineIndex = $searchIndex; 452 | 453 | break; 454 | } 455 | } 456 | 457 | $endIndex = $scopes[$currentScope]['end_index']; 458 | 459 | if (!$scopes[$currentScope]['end_index_inclusive']) { 460 | ++$endIndex; 461 | } 462 | 463 | if ( 464 | (null !== $firstNonWhitespaceTokenIndex && $firstNonWhitespaceTokenIndex < $endIndex) 465 | || (null !== $nextNewlineIndex && $nextNewlineIndex < $endIndex) 466 | ) { 467 | if ( 468 | // do we touch whitespace directly before comment... 469 | $tokens[$firstNonWhitespaceTokenIndex]->isGivenKind(T_COMMENT) 470 | // ...and afterwards, there is only comment or `}` 471 | && $tokens[$tokens->getNextMeaningfulToken($firstNonWhitespaceTokenIndex)]->equals('}') 472 | ) { 473 | if ( 474 | // ... and the comment was only content in docblock 475 | $tokens[$tokens->getPrevMeaningfulToken($firstNonWhitespaceTokenIndex)]->equals('{') 476 | ) { 477 | $indent = true; 478 | } else { 479 | // or it was dedicated comment for next control loop 480 | // ^^ we need to check if there is a control group afterwards, and in that case don't make extra indent level 481 | $nextIndex = $tokens->getNextMeaningfulToken($firstNonWhitespaceTokenIndex); 482 | $nextNextIndex = $tokens->getNextMeaningfulToken($nextIndex); 483 | 484 | if (null !== $nextNextIndex && $tokens[$nextNextIndex]->isGivenKind([T_ELSE, T_ELSEIF])) { 485 | $indent = true !== $this->configuration['stick_comment_to_next_continuous_control_statement']; 486 | } else { 487 | $indent = true; 488 | } 489 | } 490 | } else { 491 | $indent = true; 492 | } 493 | } 494 | } 495 | 496 | $previousLineInitialIndent = $this->extractIndent($content); 497 | 498 | if ($scopes[$currentScope]['skip']) { 499 | $whitespaces = $previousLineInitialIndent; 500 | } else { 501 | $whitespaces = $scopes[$currentScope]['initial_indent'].($indent ? $this->whitespacesConfig->getIndent() : ''); 502 | } 503 | 504 | $content = Preg::replace( 505 | '/(\R+)\h*$/', 506 | '$1'.$whitespaces, 507 | $content 508 | ); 509 | 510 | $previousLineNewIndent = $this->extractIndent($content); 511 | } else { 512 | $content = Preg::replace( 513 | '/(\R)'.$scopes[$currentScope]['initial_indent'].'(\h*)$/D', 514 | '$1'.$scopes[$currentScope]['new_indent'].'$2', 515 | $content 516 | ); 517 | } 518 | 519 | $lastIndent = $this->extractIndent($content); 520 | 521 | if ('' !== $previousOpenTagContent) { 522 | $content = Preg::replace("/^{$previousOpenTagContent}/", '', $content); 523 | } 524 | 525 | if ('' !== $content) { 526 | $tokens->ensureWhitespaceAtIndex($index, 0, $content); 527 | } elseif ($token->isWhitespace()) { 528 | $tokens->clearAt($index); 529 | } 530 | 531 | if (null !== $nextToken && $nextToken->isComment()) { 532 | $tokens[$index + 1] = new Token([ 533 | $nextToken->getId(), 534 | Preg::replace( 535 | '/(\R)'.preg_quote($previousLineInitialIndent, '/').'(\h*\S+.*)/', 536 | '$1'.$previousLineNewIndent.'$2', 537 | $nextToken->getContent() 538 | ), 539 | ]); 540 | } 541 | 542 | if ($token->isWhitespace()) { 543 | continue; 544 | } 545 | } 546 | 547 | if ($this->isNewLineToken($tokens, $index)) { 548 | $lastIndent = $this->extractIndent($this->computeNewLineContent($tokens, $index)); 549 | } 550 | 551 | while ($index >= $scopes[$currentScope]['end_index']) { 552 | array_pop($scopes); 553 | 554 | if ([] === $scopes) { 555 | return; 556 | } 557 | 558 | --$currentScope; 559 | } 560 | 561 | if ($token->equalsAny([';', '}', [T_OPEN_TAG], [T_CLOSE_TAG], [CT::T_ATTRIBUTE_CLOSE]])) { 562 | continue; 563 | } 564 | 565 | if ('statement' !== $scopes[$currentScope]['type'] && 'block_signature' !== $scopes[$currentScope]['type']) { 566 | $endIndex = $this->findStatementEndIndex($tokens, $index, $scopes[$currentScope]['end_index']); 567 | 568 | if ($endIndex === $index) { 569 | continue; 570 | } 571 | 572 | $scopes[] = [ 573 | 'type' => 'statement', 574 | 'skip' => false, 575 | 'end_index' => $endIndex, 576 | 'end_index_inclusive' => false, 577 | 'initial_indent' => $previousLineInitialIndent, 578 | 'new_indent' => $previousLineNewIndent, 579 | 'is_indented_block' => true, 580 | ]; 581 | } 582 | } 583 | } 584 | 585 | private function findStatementEndIndex(Tokens $tokens, int $index, int $parentScopeEndIndex): int 586 | { 587 | $endIndex = null; 588 | 589 | $ifLevel = 0; 590 | $doWhileLevel = 0; 591 | for ($searchEndIndex = $index; $searchEndIndex <= $parentScopeEndIndex; ++$searchEndIndex) { 592 | $searchEndToken = $tokens[$searchEndIndex]; 593 | 594 | if ( 595 | $searchEndToken->isGivenKind(T_IF) 596 | && !$tokens[$tokens->getPrevMeaningfulToken($searchEndIndex)]->isGivenKind(T_ELSE) 597 | ) { 598 | ++$ifLevel; 599 | 600 | continue; 601 | } 602 | 603 | if ($searchEndToken->isGivenKind(T_DO)) { 604 | ++$doWhileLevel; 605 | 606 | continue; 607 | } 608 | 609 | if ($searchEndToken->equalsAny(['(', '{', [CT::T_ARRAY_SQUARE_BRACE_OPEN]])) { 610 | if ($searchEndToken->equals('(')) { 611 | $blockType = Tokens::BLOCK_TYPE_PARENTHESIS_BRACE; 612 | } elseif ($searchEndToken->equals('{')) { 613 | $blockType = Tokens::BLOCK_TYPE_CURLY_BRACE; 614 | } else { 615 | $blockType = Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE; 616 | } 617 | 618 | $searchEndIndex = $tokens->findBlockEnd($blockType, $searchEndIndex); 619 | $searchEndToken = $tokens[$searchEndIndex]; 620 | } 621 | 622 | if (!$searchEndToken->equalsAny([';', ',', '}', [T_CLOSE_TAG]])) { 623 | continue; 624 | } 625 | 626 | $controlStructureContinuationIndex = $tokens->getNextMeaningfulToken($searchEndIndex); 627 | 628 | if ( 629 | $ifLevel > 0 630 | && null !== $controlStructureContinuationIndex 631 | && $tokens[$controlStructureContinuationIndex]->isGivenKind([T_ELSE, T_ELSEIF]) 632 | ) { 633 | if ( 634 | $tokens[$controlStructureContinuationIndex]->isGivenKind(T_ELSE) 635 | && !$tokens[$tokens->getNextMeaningfulToken($controlStructureContinuationIndex)]->isGivenKind(T_IF) 636 | ) { 637 | --$ifLevel; 638 | } 639 | 640 | $searchEndIndex = $controlStructureContinuationIndex; 641 | 642 | continue; 643 | } 644 | 645 | if ( 646 | $doWhileLevel > 0 647 | && null !== $controlStructureContinuationIndex 648 | && $tokens[$controlStructureContinuationIndex]->isGivenKind([T_WHILE]) 649 | ) { 650 | --$doWhileLevel; 651 | $searchEndIndex = $controlStructureContinuationIndex; 652 | 653 | continue; 654 | } 655 | 656 | $endIndex = $tokens->getPrevNonWhitespace($searchEndIndex); 657 | 658 | break; 659 | } 660 | 661 | return $endIndex ?? $tokens->getPrevMeaningfulToken($parentScopeEndIndex); 662 | } 663 | 664 | /** 665 | * @return array{int, bool} 666 | */ 667 | private function findCaseBlockEnd(Tokens $tokens, int $index): array 668 | { 669 | for ($max = \count($tokens); $index < $max; ++$index) { 670 | if ($tokens[$index]->isGivenKind(T_SWITCH)) { 671 | $braceIndex = $tokens->getNextMeaningfulToken( 672 | $tokens->findBlockEnd( 673 | Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 674 | $tokens->getNextMeaningfulToken($index) 675 | ) 676 | ); 677 | 678 | if ($tokens[$braceIndex]->equals(':')) { 679 | $index = $this->alternativeSyntaxAnalyzer->findAlternativeSyntaxBlockEnd($tokens, $index); 680 | } else { 681 | $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $braceIndex); 682 | } 683 | 684 | continue; 685 | } 686 | 687 | if ($tokens[$index]->equals('{')) { 688 | $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index); 689 | 690 | continue; 691 | } 692 | 693 | if ($tokens[$index]->equalsAny([[T_CASE], [T_DEFAULT]])) { 694 | return [$index, true]; 695 | } 696 | 697 | if ($tokens[$index]->equalsAny(['}', [T_ENDSWITCH]])) { 698 | return [$tokens->getPrevNonWhitespace($index), false]; 699 | } 700 | } 701 | 702 | throw new \LogicException('End of case block not found.'); 703 | } 704 | 705 | private function getLineIndentationWithBracesCompatibility(Tokens $tokens, int $index, string $regularIndent): string 706 | { 707 | if ( 708 | $this->bracesFixerCompatibility 709 | && $tokens[$index]->isGivenKind(T_OPEN_TAG) 710 | && Preg::match('/\R/', $tokens[$index]->getContent()) 711 | && isset($tokens[$index + 1]) 712 | && $tokens[$index + 1]->isWhitespace() 713 | && Preg::match('/\h+$/D', $tokens[$index + 1]->getContent()) 714 | ) { 715 | return Preg::replace('/.*?(\h+)$/sD', '$1', $tokens[$index + 1]->getContent()); 716 | } 717 | 718 | return $regularIndent; 719 | } 720 | 721 | /** 722 | * Returns whether the token at given index is the last token in a property 723 | * declaration before the type or the name of that property. 724 | */ 725 | private function isPropertyStart(Tokens $tokens, int $index): bool 726 | { 727 | $propertyKeywords = [T_VAR, T_PUBLIC, T_PROTECTED, T_PRIVATE, T_STATIC]; 728 | if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required 729 | $propertyKeywords[] = T_READONLY; 730 | } 731 | 732 | $nextIndex = $tokens->getNextMeaningfulToken($index); 733 | if ( 734 | null === $nextIndex 735 | || $tokens[$nextIndex]->isGivenKind($propertyKeywords) 736 | || $tokens[$nextIndex]->isGivenKind([T_CONST, T_FUNCTION]) 737 | ) { 738 | return false; 739 | } 740 | 741 | while ($tokens[$index]->isGivenKind($propertyKeywords)) { 742 | if ($tokens[$index]->isGivenKind([T_VAR, T_PUBLIC, T_PROTECTED, T_PRIVATE])) { 743 | return true; 744 | } 745 | 746 | $index = $tokens->getPrevMeaningfulToken($index); 747 | } 748 | 749 | return false; 750 | } 751 | 752 | /** 753 | * Returns whether the token at given index is a comment whose indentation 754 | * can be fixed. 755 | * 756 | * Indentation of a comment is not changed when the comment is part of a 757 | * multi-line message whose lines are all single-line comments and at least 758 | * one line has meaningful content. 759 | */ 760 | private function isCommentWithFixableIndentation(Tokens $tokens, int $index): bool 761 | { 762 | if (!$tokens[$index]->isComment()) { 763 | return false; 764 | } 765 | 766 | if (str_starts_with($tokens[$index]->getContent(), '/*')) { 767 | return true; 768 | } 769 | 770 | $indent = preg_quote($this->whitespacesConfig->getIndent(), '~'); 771 | 772 | if (Preg::match("~^(//|#)({$indent}.*)?$~", $tokens[$index]->getContent())) { 773 | return false; 774 | } 775 | 776 | $firstCommentIndex = $index; 777 | while (true) { 778 | $firstCommentCandidateIndex = $this->getSiblingContinuousSingleLineComment($tokens, $firstCommentIndex, false); 779 | if (null === $firstCommentCandidateIndex) { 780 | break; 781 | } 782 | 783 | $firstCommentIndex = $firstCommentCandidateIndex; 784 | } 785 | 786 | $lastCommentIndex = $index; 787 | while (true) { 788 | $lastCommentCandidateIndex = $this->getSiblingContinuousSingleLineComment($tokens, $lastCommentIndex, true); 789 | if (null === $lastCommentCandidateIndex) { 790 | break; 791 | } 792 | 793 | $lastCommentIndex = $lastCommentCandidateIndex; 794 | } 795 | 796 | if ($firstCommentIndex === $lastCommentIndex) { 797 | return true; 798 | } 799 | 800 | for ($i = $firstCommentIndex + 1; $i < $lastCommentIndex; ++$i) { 801 | if (!$tokens[$i]->isWhitespace() && !$tokens[$i]->isComment()) { 802 | return false; 803 | } 804 | } 805 | 806 | return true; 807 | } 808 | 809 | private function getSiblingContinuousSingleLineComment(Tokens $tokens, int $index, bool $after): ?int 810 | { 811 | $siblingIndex = $index; 812 | do { 813 | if ($after) { 814 | $siblingIndex = $tokens->getNextTokenOfKind($siblingIndex, [[T_COMMENT]]); 815 | } else { 816 | $siblingIndex = $tokens->getPrevTokenOfKind($siblingIndex, [[T_COMMENT]]); 817 | } 818 | 819 | if (null === $siblingIndex) { 820 | return null; 821 | } 822 | } while (str_starts_with($tokens[$siblingIndex]->getContent(), '/*')); 823 | 824 | $newLines = 0; 825 | for ($i = min($siblingIndex, $index) + 1, $max = max($siblingIndex, $index); $i < $max; ++$i) { 826 | if ($tokens[$i]->isWhitespace() && Preg::match('/\R/', $tokens[$i]->getContent())) { 827 | if (1 === $newLines || Preg::match('/\R.*\R/', $tokens[$i]->getContent())) { 828 | return null; 829 | } 830 | 831 | ++$newLines; 832 | } 833 | } 834 | 835 | return $siblingIndex; 836 | } 837 | 838 | public function getName(): string 839 | { 840 | return 'Nette/' . parent::getName(); 841 | } 842 | } 843 | -------------------------------------------------------------------------------- /src/NetteCodingStandard/Sniffs/Namespaces/OptimizeGlobalCallsSniff.php: -------------------------------------------------------------------------------- 1 | 32 | * 33 | * 34 | * 35 | * 36 | * 37 | * 38 | * 39 | * 40 | * 41 | * 42 | * 43 | * ``` 44 | */ 45 | 46 | namespace NetteCodingStandard\Sniffs\Namespaces; 47 | 48 | use Exception; 49 | use PHP_CodeSniffer\Files\File; 50 | use PHP_CodeSniffer\Sniffs\Sniff; 51 | use function count, defined, in_array; 52 | 53 | 54 | class OptimizeGlobalCallsSniff implements Sniff 55 | { 56 | public $optimizedFunctionsOnly = true; 57 | public $ignoredFunctions = []; 58 | public $ignoredConstants = []; 59 | private static $processedFiles = []; 60 | 61 | private $compilerOptimizedFunctions = [ 62 | 'strlen', 'is_null', 'is_bool', 'is_long', 'is_int', 'is_integer', 63 | 'is_float', 'is_double', 'is_string', 'is_array', 'is_object', 64 | 'is_resource', 'is_scalar', 'boolval', 'intval', 'floatval', 65 | 'doubleval', 'strval', 'defined', 'chr', 'ord', 'call_user_func_array', 66 | 'call_user_func', 'in_array', 'count', 'sizeof', 'get_class', 67 | 'get_called_class', 'gettype', 'func_num_args', 'func_get_args', 68 | 'array_slice', 'array_key_exists', 'sprintf', 69 | ]; 70 | 71 | private $builtInIgnoredConstants = [ 72 | 'TRUE', 'FALSE', 'NULL', 73 | ]; 74 | 75 | 76 | public function register(): array 77 | { 78 | return [T_OPEN_TAG]; 79 | } 80 | 81 | 82 | public function process(File $phpcsFile, $stackPtr) 83 | { 84 | if ($stackPtr > 0) { 85 | return; 86 | } 87 | 88 | $filename = $phpcsFile->getFilename(); 89 | 90 | if (isset(self::$processedFiles[$filename])) { 91 | return; 92 | } 93 | 94 | try { 95 | if (!$this->hasNamespace($phpcsFile)) { 96 | self::$processedFiles[$filename] = true; 97 | return; 98 | } 99 | 100 | $existingUseStatements = $this->findExistingUseStatements($phpcsFile); 101 | 102 | $usedFunctions = $this->findUsedGlobalFunctions($phpcsFile, $existingUseStatements); 103 | $usedConstants = $this->findUsedGlobalConstants($phpcsFile, $existingUseStatements); 104 | 105 | $finalFunctions = $usedFunctions; 106 | if ($this->optimizedFunctionsOnly) { 107 | $nonOptimizedToKeep = []; 108 | foreach ($existingUseStatements['all_functions'] as $name) { 109 | if (!in_array(strtolower($name), $this->compilerOptimizedFunctions, true)) { 110 | if ($this->isFunctionUsedInCode($phpcsFile, $name)) { 111 | $nonOptimizedToKeep[] = $name; 112 | } 113 | } 114 | } 115 | $finalFunctions = array_values(array_unique(array_merge($finalFunctions, $nonOptimizedToKeep))); 116 | } 117 | 118 | $finalConstants = $usedConstants; 119 | 120 | $isCorrect = $this->isStateCorrect($phpcsFile, $finalFunctions, $finalConstants, $existingUseStatements); 121 | $hasBackslashesToRemove = $this->hasBackslashesToRemove($phpcsFile, $finalFunctions, $finalConstants); 122 | 123 | if ($isCorrect && !$hasBackslashesToRemove) { 124 | self::$processedFiles[$filename] = true; 125 | return; 126 | } 127 | 128 | $fixMessage = 'Global functions and constants should be imported via `use` statements for performance and clarity.'; 129 | $fix = $phpcsFile->addFixableError($fixMessage, $stackPtr, 'ImportGlobalSymbols'); 130 | 131 | if ($fix === true) { 132 | $success = $this->applyFixWithErrorHandling($phpcsFile, $finalFunctions, $finalConstants, $existingUseStatements); 133 | if ($success) { 134 | self::$processedFiles[$filename] = true; 135 | } 136 | } 137 | } catch (\Throwable $e) { 138 | return; 139 | } 140 | } 141 | 142 | 143 | private function applyFixWithErrorHandling( 144 | File $phpcsFile, 145 | array $finalFunctions, 146 | array $finalConstants, 147 | array $existingUseStatements, 148 | ): bool 149 | { 150 | try { 151 | $phpcsFile->fixer->beginChangeset(); 152 | 153 | $this->processUseStatements($phpcsFile, 'function', $finalFunctions, $existingUseStatements['functions']); 154 | $this->processUseStatements($phpcsFile, 'const', $finalConstants, $existingUseStatements['constants']); 155 | $this->removeBackslashesFromCode($phpcsFile, $finalFunctions, $finalConstants); 156 | 157 | $phpcsFile->fixer->endChangeset(); 158 | return true; 159 | } catch (\Throwable $e) { 160 | $phpcsFile->fixer->rollbackChangeset(); 161 | return false; 162 | } 163 | } 164 | 165 | 166 | private function processUseStatements(File $phpcsFile, string $type, array $finalNames, array $existingStmts) 167 | { 168 | if (empty($existingStmts) && empty($finalNames)) { 169 | return; 170 | } 171 | 172 | if (empty($finalNames)) { 173 | foreach ($existingStmts as $stmt) { 174 | $this->deleteUseStatement($phpcsFile, $stmt); 175 | } 176 | return; 177 | } 178 | 179 | if (empty($existingStmts)) { 180 | $this->addNewUseBlock($phpcsFile, $type, $finalNames); 181 | return; 182 | } 183 | 184 | $mainStmt = array_shift($existingStmts); 185 | foreach ($existingStmts as $stmt) { 186 | $this->deleteUseStatement($phpcsFile, $stmt); 187 | } 188 | 189 | $this->replaceUseContent($phpcsFile, $mainStmt, $finalNames); 190 | } 191 | 192 | 193 | private function deleteUseStatement(File $phpcsFile, array $statement) 194 | { 195 | $lineStart = $statement['start']; 196 | while ($lineStart > 0 && $phpcsFile->getTokens()[$lineStart - 1]['line'] === $statement['line']) { 197 | $lineStart--; 198 | } 199 | 200 | $lineEnd = $statement['end']; 201 | if ( 202 | isset($phpcsFile->getTokens()[$lineEnd + 1]) 203 | && preg_match('/^(\r\n|\n|\r)/', $phpcsFile->getTokens()[$lineEnd + 1]['content']) 204 | ) { 205 | $lineEnd++; 206 | } 207 | 208 | for ($i = $lineStart; $i <= $lineEnd; $i++) { 209 | $phpcsFile->fixer->replaceToken($i, ''); 210 | } 211 | } 212 | 213 | 214 | private function replaceUseContent(File $phpcsFile, array $statement, array $finalNames) 215 | { 216 | sort($finalNames); 217 | $startContentPtr = $phpcsFile->findNext(T_STRING, $statement['start']); 218 | $startContentPtr = $phpcsFile->findNext(T_WHITESPACE, $startContentPtr + 1, null, true); 219 | $endContentPtr = $phpcsFile->findPrevious(T_SEMICOLON, $statement['end']); 220 | 221 | for ($i = $startContentPtr; $i < $endContentPtr; $i++) { 222 | $phpcsFile->fixer->replaceToken($i, ''); 223 | } 224 | 225 | $phpcsFile->fixer->addContentBefore($endContentPtr, implode(', ', $finalNames)); 226 | } 227 | 228 | 229 | private function addNewUseBlock(File $phpcsFile, string $type, array $names) 230 | { 231 | $insertPointInfo = $this->findInsertionPointInfo($phpcsFile); 232 | if ($insertPointInfo === null) { 233 | return; 234 | } 235 | 236 | $insertPosition = $insertPointInfo['position']; 237 | $afterType = $insertPointInfo['after']; 238 | 239 | sort($names); 240 | $eol = $phpcsFile->eolChar; 241 | $content = 'use ' . $type . ' ' . implode(', ', $names) . ';'; 242 | 243 | $prefix = ($afterType === 'namespace') ? $eol . $eol : $eol; 244 | 245 | $phpcsFile->fixer->addContent($insertPosition, $prefix . $content); 246 | } 247 | 248 | 249 | private function isStateCorrect( 250 | File $phpcsFile, 251 | array $finalFunctions, 252 | array $finalConstants, 253 | array $existingUseStatements, 254 | ): bool 255 | { 256 | sort($finalFunctions); 257 | sort($finalConstants); 258 | 259 | $currentFunctions = $existingUseStatements['all_functions']; 260 | $currentConstants = $existingUseStatements['all_constants']; 261 | sort($currentFunctions); 262 | sort($currentConstants); 263 | 264 | if ($finalFunctions !== $currentFunctions) { 265 | return false; 266 | } 267 | 268 | if (!$this->constantArraysMatch($finalConstants, $currentConstants)) { 269 | return false; 270 | } 271 | 272 | return count($existingUseStatements['functions']) <= 1 && count($existingUseStatements['constants']) <= 1; 273 | } 274 | 275 | 276 | private function isFunctionUsedInCode(File $phpcsFile, string $functionName): bool 277 | { 278 | $tokens = $phpcsFile->getTokens(); 279 | for ($i = 0; $i < $phpcsFile->numTokens; $i++) { 280 | if ($tokens[$i]['code'] === T_STRING && strtolower($tokens[$i]['content']) === strtolower($functionName)) { 281 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, $i + 1, null, true); 282 | if ($nextToken !== false && $tokens[$nextToken]['code'] === T_OPEN_PARENTHESIS) { 283 | $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, $i - 1, null, true); 284 | if ( 285 | $prevToken === false 286 | || !in_array($tokens[$prevToken]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON, T_NULLSAFE_OBJECT_OPERATOR, T_FUNCTION], true) 287 | ) { 288 | return true; 289 | } 290 | } 291 | } 292 | } 293 | return false; 294 | } 295 | 296 | 297 | private function isFunctionDeclaration(File $phpcsFile, int $stackPtr): bool 298 | { 299 | $prevSemicolon = $phpcsFile->findPrevious(T_SEMICOLON, $stackPtr - 1); 300 | $prevOpenBrace = $phpcsFile->findPrevious(T_OPEN_CURLY_BRACKET, $stackPtr - 1); 301 | $boundary = max($prevSemicolon, $prevOpenBrace); 302 | if ($boundary === false) { 303 | $boundary = null; 304 | } 305 | $functionKeyword = $phpcsFile->findPrevious(T_FUNCTION, $stackPtr - 1, $boundary); 306 | return $functionKeyword !== false; 307 | } 308 | 309 | 310 | private function isWithinUseStatement(int $stackPtr, array $existingUseStatements): bool 311 | { 312 | $allStatements = array_merge($existingUseStatements['functions'], $existingUseStatements['constants']); 313 | foreach ($allStatements as $statement) { 314 | if ($stackPtr >= $statement['start'] && $stackPtr <= $statement['end']) { 315 | return true; 316 | } 317 | } 318 | return false; 319 | } 320 | 321 | 322 | private function findUsedGlobalFunctions(File $phpcsFile, array $existingUseStatements): array 323 | { 324 | $tokens = $phpcsFile->getTokens(); 325 | $usedFunctions = []; 326 | $ignoredFunctions = array_map('strtolower', $this->ignoredFunctions); 327 | 328 | for ($i = 0; $i < $phpcsFile->numTokens; $i++) { 329 | if ( 330 | $this->isWithinUseStatement($i, $existingUseStatements) 331 | || $this->isFunctionDeclaration($phpcsFile, $i) 332 | ) { 333 | continue; 334 | } 335 | 336 | if ($tokens[$i]['code'] !== T_STRING) { 337 | continue; 338 | } 339 | 340 | $functionName = $tokens[$i]['content']; 341 | if (in_array(strtolower($functionName), $ignoredFunctions, true)) { 342 | continue; 343 | } 344 | 345 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, $i + 1, null, true); 346 | if ($nextToken === false || $tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS) { 347 | continue; 348 | } 349 | 350 | $prevTokenPtr = $phpcsFile->findPrevious(T_WHITESPACE, $i - 1, null, true); 351 | if ( 352 | $prevTokenPtr !== false 353 | && in_array($tokens[$prevTokenPtr]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON, T_NULLSAFE_OBJECT_OPERATOR], true) 354 | ) { 355 | continue; 356 | } 357 | 358 | if ($prevTokenPtr !== false && $tokens[$prevTokenPtr]['code'] === T_NS_SEPARATOR) { 359 | $beforeBackslash = $phpcsFile->findPrevious(T_WHITESPACE, $prevTokenPtr - 1, null, true); 360 | if ($beforeBackslash !== false && $tokens[$beforeBackslash]['code'] === T_STRING) { 361 | continue; 362 | } 363 | } 364 | 365 | if ($this->optimizedFunctionsOnly) { 366 | if (!in_array(strtolower($functionName), $this->compilerOptimizedFunctions, true)) { 367 | continue; 368 | } 369 | } else { 370 | if (!function_exists($functionName)) { 371 | continue; 372 | } 373 | } 374 | $usedFunctions[] = $functionName; 375 | } 376 | return array_values(array_unique($usedFunctions)); 377 | } 378 | 379 | 380 | private function findUsedGlobalConstants(File $phpcsFile, array $existingUseStatements): array 381 | { 382 | $tokens = $phpcsFile->getTokens(); 383 | $usedConstants = []; 384 | 385 | for ($i = 0; $i < $phpcsFile->numTokens; $i++) { 386 | if ($this->isWithinUseStatement($i, $existingUseStatements)) { 387 | continue; 388 | } 389 | 390 | if ($tokens[$i]['code'] !== T_STRING) { 391 | continue; 392 | } 393 | 394 | $constantName = $tokens[$i]['content']; 395 | if ($this->isIgnoredConstant($constantName)) { 396 | continue; 397 | } 398 | 399 | $prevTokenPtr = $phpcsFile->findPrevious(T_WHITESPACE, $i - 1, null, true); 400 | $nextTokenPtr = $phpcsFile->findNext(T_WHITESPACE, $i + 1, null, true); 401 | 402 | if ($nextTokenPtr !== false && $tokens[$nextTokenPtr]['code'] === T_OPEN_PARENTHESIS) { 403 | continue; 404 | } 405 | 406 | if ( 407 | $prevTokenPtr !== false 408 | && in_array($tokens[$prevTokenPtr]['code'], [T_OBJECT_OPERATOR, T_DOUBLE_COLON, T_NULLSAFE_OBJECT_OPERATOR, T_COLON], true) 409 | ) { 410 | continue; 411 | } 412 | 413 | if ($prevTokenPtr !== false && $tokens[$prevTokenPtr]['code'] === T_NS_SEPARATOR) { 414 | $beforeBackslash = $phpcsFile->findPrevious(T_WHITESPACE, $prevTokenPtr - 1, null, true); 415 | if ($beforeBackslash !== false && $tokens[$beforeBackslash]['code'] === T_STRING) { 416 | continue; 417 | } 418 | } 419 | 420 | if ( 421 | $nextTokenPtr !== false 422 | && ( 423 | $tokens[$nextTokenPtr]['code'] === T_DOUBLE_COLON 424 | || $tokens[$nextTokenPtr]['code'] === T_NS_SEPARATOR 425 | ) 426 | ) { 427 | continue; 428 | } 429 | 430 | if (!defined($constantName)) { 431 | continue; 432 | } 433 | $usedConstants[] = $constantName; 434 | } 435 | return array_values(array_unique($usedConstants)); 436 | } 437 | 438 | 439 | private function findInsertionPointInfo(File $phpcsFile): ?array 440 | { 441 | $tokens = $phpcsFile->getTokens(); 442 | $lastUsePos = null; 443 | 444 | for ($i = $phpcsFile->numTokens - 1; $i >= 0; $i--) { 445 | if ($tokens[$i]['code'] === T_USE && $this->isTopLevelUseStatement($phpcsFile, $i)) { 446 | $lastUsePos = $i; 447 | break; 448 | } 449 | } 450 | 451 | if ($lastUsePos !== null) { 452 | $semicolonPos = $phpcsFile->findNext(T_SEMICOLON, $lastUsePos); 453 | if ($semicolonPos !== false) { 454 | return ['position' => $semicolonPos, 'after' => 'use']; 455 | } 456 | } 457 | 458 | $namespacePos = $phpcsFile->findNext(T_NAMESPACE, 0); 459 | if ($namespacePos !== false) { 460 | $semicolonPos = $phpcsFile->findNext(T_SEMICOLON, $namespacePos); 461 | if ($semicolonPos !== false) { 462 | return ['position' => $semicolonPos, 'after' => 'namespace']; 463 | } 464 | } 465 | 466 | return null; 467 | } 468 | 469 | 470 | private function findExistingUseStatements(File $phpcsFile): array 471 | { 472 | $tokens = $phpcsFile->getTokens(); 473 | $useStatements = ['functions' => [], 'constants' => [], 'all_functions' => [], 'all_constants' => []]; 474 | 475 | for ($i = 0; $i < count($tokens); $i++) { 476 | if ($tokens[$i]['code'] !== T_USE) { 477 | continue; 478 | } 479 | 480 | $useStatement = $this->parseUseStatement($phpcsFile, $i); 481 | if ($useStatement === null || !$this->isTopLevelUseStatement($phpcsFile, $i)) { 482 | continue; 483 | } 484 | 485 | if ($useStatement['type'] === 'function' && $useStatement['is_global']) { 486 | $useStatements['functions'][] = $useStatement; 487 | $useStatements['all_functions'] = array_merge($useStatements['all_functions'], $useStatement['names']); 488 | } elseif ($useStatement['type'] === 'const' && $useStatement['is_global']) { 489 | $useStatements['constants'][] = $useStatement; 490 | $useStatements['all_constants'] = array_merge($useStatements['all_constants'], $useStatement['names']); 491 | } 492 | } 493 | $useStatements['all_functions'] = array_values(array_unique($useStatements['all_functions'])); 494 | $useStatements['all_constants'] = array_values(array_unique($useStatements['all_constants'])); 495 | return $useStatements; 496 | } 497 | 498 | 499 | private function parseUseStatement(File $phpcsFile, int $usePtr): ?array 500 | { 501 | $tokens = $phpcsFile->getTokens(); 502 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, $usePtr + 1, null, true); 503 | if ($nextToken === false) { 504 | return null; 505 | } 506 | 507 | $type = null; 508 | $startNamePtr = $nextToken; 509 | 510 | if ($tokens[$nextToken]['code'] === T_STRING) { 511 | $content = strtolower($tokens[$nextToken]['content']); 512 | if ($content === 'function' || $content === 'const') { 513 | $type = $content; 514 | $startNamePtr = $phpcsFile->findNext(T_WHITESPACE, $nextToken + 1, null, true); 515 | } 516 | } 517 | 518 | if ($startNamePtr === false) { 519 | return null; 520 | } 521 | 522 | $endPtr = $phpcsFile->findNext(T_SEMICOLON, $startNamePtr); 523 | if ($endPtr === false) { 524 | return null; 525 | } 526 | 527 | $names = []; 528 | $currentName = ''; 529 | $isGlobal = true; 530 | 531 | for ($i = $startNamePtr; $i < $endPtr; $i++) { 532 | if ($tokens[$i]['code'] === T_WHITESPACE) { 533 | continue; 534 | } 535 | if ($tokens[$i]['content'] === ',') { 536 | if ($currentName !== '') { 537 | $names[] = trim($currentName); 538 | $currentName = ''; 539 | } 540 | continue; 541 | } 542 | if ($tokens[$i]['code'] === T_NS_SEPARATOR && $currentName !== '') { 543 | $isGlobal = false; 544 | } 545 | $currentName .= $tokens[$i]['content']; 546 | } 547 | 548 | if ($currentName !== '') { 549 | $names[] = trim($currentName); 550 | } 551 | 552 | return [ 553 | 'start' => $usePtr, 'end' => $endPtr, 'type' => $type, 554 | 'names' => $names, 'is_global' => $isGlobal, 'line' => $tokens[$usePtr]['line'], 555 | ]; 556 | } 557 | 558 | 559 | private function removeBackslashesFromCode(File $phpcsFile, array $usedFunctions, array $usedConstants): void 560 | { 561 | if (empty($usedFunctions) && empty($usedConstants)) { 562 | return; 563 | } 564 | 565 | $tokens = $phpcsFile->getTokens(); 566 | $lookups = $this->createLookupArrays($usedFunctions, $usedConstants); 567 | 568 | for ($i = 0; $i < $phpcsFile->numTokens; $i++) { 569 | if ($tokens[$i]['code'] !== T_NS_SEPARATOR) { 570 | continue; 571 | } 572 | 573 | $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, $i - 1, null, true); 574 | if ($prevToken !== false && $tokens[$prevToken]['code'] === T_STRING) { 575 | continue; 576 | } 577 | 578 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, $i + 1, null, true); 579 | if ($nextToken === false || $tokens[$nextToken]['code'] !== T_STRING) { 580 | continue; 581 | } 582 | 583 | $name = $tokens[$nextToken]['content']; 584 | $nameLower = strtolower($name); 585 | $afterName = $phpcsFile->findNext(T_WHITESPACE, $nextToken + 1, null, true); 586 | 587 | if ($afterName !== false && $tokens[$afterName]['code'] === T_OPEN_PARENTHESIS) { 588 | if (isset($lookups['functions'][$nameLower])) { 589 | $phpcsFile->fixer->replaceToken($i, ''); 590 | } 591 | } elseif ($this->isConstantContext($afterName, $tokens)) { 592 | if (isset($lookups['constants'][$nameLower])) { 593 | $phpcsFile->fixer->replaceToken($i, ''); 594 | } 595 | } 596 | } 597 | } 598 | 599 | 600 | private function hasNamespace(File $phpcsFile): bool 601 | { 602 | return $phpcsFile->findNext(T_NAMESPACE, 0) !== false; 603 | } 604 | 605 | 606 | private function isIgnoredConstant(string $constantName): bool 607 | { 608 | $ignored = array_merge($this->builtInIgnoredConstants, $this->ignoredConstants); 609 | foreach ($ignored as $pattern) { 610 | $regex = str_replace('\*', '.*', preg_quote($pattern, '/')); 611 | if (preg_match('/^' . $regex . '$/i', $constantName)) { 612 | return true; 613 | } 614 | } 615 | 616 | return false; 617 | } 618 | 619 | 620 | private function createLookupArrays(array $usedFunctions, array $usedConstants): array 621 | { 622 | $functionLookup = []; 623 | foreach ($usedFunctions as $func) { 624 | $functionLookup[strtolower($func)] = $func; 625 | } 626 | $constantLookup = []; 627 | foreach ($usedConstants as $const) { 628 | $constantLookup[strtolower($const)] = $const; 629 | } 630 | return ['functions' => $functionLookup, 'constants' => $constantLookup]; 631 | } 632 | 633 | 634 | private function hasBackslashesToRemove(File $phpcsFile, array $usedFunctions, array $usedConstants): bool 635 | { 636 | if (empty($usedFunctions) && empty($usedConstants)) { 637 | return false; 638 | } 639 | 640 | $tokens = $phpcsFile->getTokens(); 641 | $lookups = $this->createLookupArrays($usedFunctions, $usedConstants); 642 | 643 | for ($i = 0; $i < count($tokens); $i++) { 644 | if ($tokens[$i]['code'] !== T_NS_SEPARATOR) { 645 | continue; 646 | } 647 | 648 | $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, $i - 1, null, true); 649 | if ($prevToken !== false && $tokens[$prevToken]['code'] === T_STRING) { 650 | continue; 651 | } 652 | 653 | $nextToken = $phpcsFile->findNext(T_WHITESPACE, $i + 1, null, true); 654 | if ($nextToken === false || $tokens[$nextToken]['code'] !== T_STRING) { 655 | continue; 656 | } 657 | 658 | $name = $tokens[$nextToken]['content']; 659 | $nameLower = strtolower($name); 660 | $afterName = $phpcsFile->findNext(T_WHITESPACE, $nextToken + 1, null, true); 661 | 662 | if ($afterName !== false && $tokens[$afterName]['code'] === T_OPEN_PARENTHESIS) { 663 | if (isset($lookups['functions'][$nameLower])) { 664 | return true; 665 | } 666 | } elseif ($this->isConstantContext($afterName, $tokens)) { 667 | if (isset($lookups['constants'][$nameLower])) { 668 | return true; 669 | } 670 | } 671 | } 672 | return false; 673 | } 674 | 675 | 676 | private function constantArraysMatch(array $existing, array $required): bool 677 | { 678 | if (count($existing) !== count($required)) { 679 | return false; 680 | } 681 | $existingNormalized = array_map('strtolower', $existing); 682 | $requiredNormalized = array_map('strtolower', $required); 683 | sort($existingNormalized); 684 | sort($requiredNormalized); 685 | return $existingNormalized === $requiredNormalized; 686 | } 687 | 688 | 689 | private function isConstantContext(?int $afterNamePos, array $tokens): bool 690 | { 691 | if ($afterNamePos === null || !isset($tokens[$afterNamePos])) { 692 | return true; 693 | } 694 | $tokenCode = $tokens[$afterNamePos]['code']; 695 | return !in_array($tokenCode, [T_OPEN_PARENTHESIS, T_DOUBLE_COLON, T_NS_SEPARATOR], true); 696 | } 697 | 698 | 699 | private function isTopLevelUseStatement(File $phpcsFile, int $usePos): bool 700 | { 701 | return empty($phpcsFile->getTokens()[$usePos]['conditions']); 702 | } 703 | } 704 | -------------------------------------------------------------------------------- /src/NetteCodingStandard/Sniffs/WhiteSpace/FunctionSpacingSniff.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) 7 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 8 | */ 9 | 10 | namespace NetteCodingStandard\Sniffs\WhiteSpace; 11 | 12 | use PHP_CodeSniffer\Files\File; 13 | use PHP_CodeSniffer\Sniffs\Sniff; 14 | use PHP_CodeSniffer\Util\Tokens; 15 | 16 | class FunctionSpacingSniff implements Sniff 17 | { 18 | 19 | /** 20 | * The number of blank lines between functions. 21 | * 22 | * @var integer 23 | */ 24 | public $spacing = 2; 25 | 26 | /** 27 | * The number of blank lines before the first function in a class. 28 | * 29 | * @var integer 30 | */ 31 | public $spacingBeforeFirst = 2; 32 | 33 | /** 34 | * The number of blank lines after the last function in a class. 35 | * 36 | * @var integer 37 | */ 38 | public $spacingAfterLast = 2; 39 | 40 | /** 41 | * Original properties as set in a custom ruleset (if any). 42 | * 43 | * @var array|null 44 | */ 45 | private $rulesetProperties = null; 46 | 47 | 48 | /** 49 | * Returns an array of tokens this test wants to listen for. 50 | * 51 | * @return array 52 | */ 53 | public function register() 54 | { 55 | return [T_FUNCTION]; 56 | 57 | }//end register() 58 | 59 | 60 | /** 61 | * Processes this sniff when one of its tokens is encountered. 62 | * 63 | * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 64 | * @param int $stackPtr The position of the current token 65 | * in the stack passed in $tokens. 66 | * 67 | * @return void 68 | */ 69 | public function process(File $phpcsFile, $stackPtr) 70 | { 71 | $tokens = $phpcsFile->getTokens(); 72 | $previousNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); 73 | if ($previousNonEmpty !== false 74 | && $tokens[$previousNonEmpty]['code'] === T_OPEN_TAG 75 | && $tokens[$previousNonEmpty]['line'] !== 1 76 | ) { 77 | // Ignore functions at the start of an embedded PHP block. 78 | return; 79 | } 80 | 81 | $this->spacing = end($tokens[$stackPtr]['conditions']) === T_INTERFACE ? 1 : 2; 82 | 83 | // If the ruleset has only overridden the spacing property, use 84 | // that value for all spacing rules. 85 | if ($this->rulesetProperties === null) { 86 | $this->rulesetProperties = []; 87 | if (isset($phpcsFile->ruleset->ruleset['Squiz.WhiteSpace.FunctionSpacing']) === true 88 | && isset($phpcsFile->ruleset->ruleset['Squiz.WhiteSpace.FunctionSpacing']['properties']) === true 89 | ) { 90 | $this->rulesetProperties = $phpcsFile->ruleset->ruleset['Squiz.WhiteSpace.FunctionSpacing']['properties']; 91 | if (isset($this->rulesetProperties['spacing']) === true) { 92 | if (isset($this->rulesetProperties['spacingBeforeFirst']) === false) { 93 | $this->spacingBeforeFirst = $this->spacing; 94 | } 95 | 96 | if (isset($this->rulesetProperties['spacingAfterLast']) === false) { 97 | $this->spacingAfterLast = $this->spacing; 98 | } 99 | } 100 | } 101 | } 102 | 103 | $this->spacing = (int) $this->spacing; 104 | $this->spacingBeforeFirst = (int) $this->spacingBeforeFirst; 105 | $this->spacingAfterLast = (int) $this->spacingAfterLast; 106 | 107 | if (isset($tokens[$stackPtr]['scope_closer']) === false) { 108 | // Must be an interface method, so the closer is the semicolon. 109 | $closer = $phpcsFile->findNext(T_SEMICOLON, $stackPtr); 110 | } else { 111 | $closer = $tokens[$stackPtr]['scope_closer']; 112 | } 113 | 114 | $isFirst = false; 115 | $isLast = false; 116 | 117 | $ignore = ([T_WHITESPACE => T_WHITESPACE] + Tokens::$methodPrefixes); 118 | 119 | $prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); 120 | 121 | while ($tokens[$prev]['code'] === T_ATTRIBUTE_END) { 122 | // Skip past function attributes. 123 | $prev = $phpcsFile->findPrevious($ignore, ($tokens[$prev]['attribute_opener'] - 1), null, true); 124 | } 125 | 126 | if ($tokens[$prev]['code'] === T_DOC_COMMENT_CLOSE_TAG) { 127 | // Skip past function docblocks. 128 | $prev = $phpcsFile->findPrevious($ignore, ($tokens[$prev]['comment_opener'] - 1), null, true); 129 | } 130 | 131 | while ($tokens[$prev]['code'] === T_ATTRIBUTE_END) { 132 | // Skip past function attributes. 133 | $prev = $phpcsFile->findPrevious($ignore, ($tokens[$prev]['attribute_opener'] - 1), null, true); 134 | } 135 | 136 | if ($tokens[$prev]['code'] === T_OPEN_CURLY_BRACKET) { 137 | $isFirst = true; 138 | } 139 | 140 | $next = $phpcsFile->findNext($ignore, ($closer + 1), null, true); 141 | if (isset(Tokens::$emptyTokens[$tokens[$next]['code']]) === true 142 | && $tokens[$next]['line'] === $tokens[$closer]['line'] 143 | ) { 144 | // Skip past "end" comments. 145 | $next = $phpcsFile->findNext($ignore, ($next + 1), null, true); 146 | } 147 | 148 | if ($tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET) { 149 | $isLast = true; 150 | } 151 | 152 | /* 153 | Check the number of blank lines 154 | after the function. 155 | */ 156 | 157 | // Allow for comments on the same line as the closer. 158 | for ($nextLineToken = ($closer + 1); $nextLineToken < $phpcsFile->numTokens; $nextLineToken++) { 159 | if ($tokens[$nextLineToken]['line'] !== $tokens[$closer]['line']) { 160 | break; 161 | } 162 | } 163 | 164 | $requiredSpacing = $this->spacing; 165 | $errorCode = 'After'; 166 | if ($isLast === true) { 167 | $requiredSpacing = $this->spacingAfterLast; 168 | $errorCode = 'AfterLast'; 169 | } 170 | 171 | $foundLines = 0; 172 | if ($nextLineToken === ($phpcsFile->numTokens - 1)) { 173 | // We are at the end of the file. 174 | // Don't check spacing after the function because this 175 | // should be done by an EOF sniff. 176 | $foundLines = $requiredSpacing; 177 | } else { 178 | $nextContent = $phpcsFile->findNext(T_WHITESPACE, $nextLineToken, null, true); 179 | if ($nextContent === false) { 180 | // We are at the end of the file. 181 | // Don't check spacing after the function because this 182 | // should be done by an EOF sniff. 183 | $foundLines = $requiredSpacing; 184 | } else { 185 | $foundLines = ($tokens[$nextContent]['line'] - $tokens[$nextLineToken]['line']); 186 | } 187 | } 188 | 189 | if ($isLast === true) { 190 | $phpcsFile->recordMetric($stackPtr, 'Function spacing after last', $foundLines); 191 | } else { 192 | $phpcsFile->recordMetric($stackPtr, 'Function spacing after', $foundLines); 193 | } 194 | 195 | if ($foundLines !== $requiredSpacing) { 196 | $error = 'Expected %s blank line'; 197 | if ($requiredSpacing !== 1) { 198 | $error .= 's'; 199 | } 200 | 201 | $error .= ' after function; %s found'; 202 | $data = [ 203 | $requiredSpacing, 204 | $foundLines, 205 | ]; 206 | 207 | $fix = $phpcsFile->addFixableError($error, $closer, $errorCode, $data); 208 | if ($fix === true) { 209 | $phpcsFile->fixer->beginChangeset(); 210 | for ($i = $nextLineToken; $i <= $nextContent; $i++) { 211 | if ($tokens[$i]['line'] === $tokens[$nextContent]['line']) { 212 | $phpcsFile->fixer->addContentBefore($i, str_repeat($phpcsFile->eolChar, $requiredSpacing)); 213 | break; 214 | } 215 | 216 | $phpcsFile->fixer->replaceToken($i, ''); 217 | } 218 | 219 | $phpcsFile->fixer->endChangeset(); 220 | }//end if 221 | }//end if 222 | 223 | /* 224 | Check the number of blank lines 225 | before the function. 226 | */ 227 | 228 | $prevLineToken = null; 229 | for ($i = $stackPtr; $i >= 0; $i--) { 230 | if ($tokens[$i]['line'] === $tokens[$stackPtr]['line']) { 231 | continue; 232 | } 233 | 234 | $prevLineToken = $i; 235 | break; 236 | } 237 | 238 | if ($prevLineToken === null) { 239 | // Never found the previous line, which means 240 | // there are 0 blank lines before the function. 241 | $foundLines = 0; 242 | $prevContent = 0; 243 | $prevLineToken = 0; 244 | } else { 245 | $currentLine = $tokens[$stackPtr]['line']; 246 | 247 | $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, $prevLineToken, null, true); 248 | 249 | if ($tokens[$prevContent]['code'] === T_COMMENT 250 | || isset(Tokens::$phpcsCommentTokens[$tokens[$prevContent]['code']]) === true 251 | ) { 252 | // Ignore comments as they can have different spacing rules, and this 253 | // isn't a proper function comment anyway. 254 | return; 255 | } 256 | 257 | while ($tokens[$prevContent]['code'] === T_ATTRIBUTE_END 258 | && $tokens[$prevContent]['line'] === ($currentLine - 1) 259 | ) { 260 | // Account for function attributes. 261 | $currentLine = $tokens[$tokens[$prevContent]['attribute_opener']]['line']; 262 | $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$prevContent]['attribute_opener'] - 1), null, true); 263 | } 264 | 265 | if ($tokens[$prevContent]['code'] === T_DOC_COMMENT_CLOSE_TAG 266 | && $tokens[$prevContent]['line'] === ($currentLine - 1) 267 | ) { 268 | // Account for function comments. 269 | $currentLine = $tokens[$tokens[$prevContent]['comment_opener']]['line']; 270 | $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$prevContent]['comment_opener'] - 1), null, true); 271 | } 272 | 273 | while ($tokens[$prevContent]['code'] === T_ATTRIBUTE_END 274 | && $tokens[$prevContent]['line'] === ($currentLine - 1) 275 | ) { 276 | // Account for function attributes. 277 | $currentLine = $tokens[$tokens[$prevContent]['attribute_opener']]['line']; 278 | $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$prevContent]['attribute_opener'] - 1), null, true); 279 | } 280 | 281 | if ($tokens[$prevContent]['level'] && ($useToken = $phpcsFile->findPrevious(T_USE, $prevContent)) && $tokens[$useToken]['line'] === $tokens[$prevContent]['line'] ) { 282 | $this->spacing = 1; // method after 'use' 283 | } 284 | 285 | $prevLineToken = $prevContent; 286 | 287 | // Before we throw an error, check that we are not throwing an error 288 | // for another function. We don't want to error for no blank lines after 289 | // the previous function and no blank lines before this one as well. 290 | $prevLine = ($tokens[$prevContent]['line'] - 1); 291 | $i = ($stackPtr - 1); 292 | $foundLines = 0; 293 | 294 | $stopAt = 0; 295 | if (isset($tokens[$stackPtr]['conditions']) === true) { 296 | $conditions = $tokens[$stackPtr]['conditions']; 297 | $conditions = array_keys($conditions); 298 | $stopAt = array_pop($conditions); 299 | } 300 | 301 | while ($currentLine !== $prevLine && $currentLine > 1 && $i > $stopAt) { 302 | if ($tokens[$i]['code'] === T_FUNCTION) { 303 | // Found another interface or abstract function. 304 | return; 305 | } 306 | 307 | if ($tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET 308 | && $tokens[$tokens[$i]['scope_condition']]['code'] === T_FUNCTION 309 | ) { 310 | // Found a previous function. 311 | return; 312 | } 313 | 314 | $currentLine = $tokens[$i]['line']; 315 | if ($currentLine === $prevLine) { 316 | break; 317 | } 318 | 319 | if ($tokens[($i - 1)]['line'] < $currentLine && $tokens[($i + 1)]['line'] > $currentLine) { 320 | // This token is on a line by itself. If it is whitespace, the line is empty. 321 | if ($tokens[$i]['code'] === T_WHITESPACE) { 322 | $foundLines++; 323 | } 324 | } 325 | 326 | $i--; 327 | }//end while 328 | }//end if 329 | 330 | $requiredSpacing = $this->spacing; 331 | $errorCode = 'Before'; 332 | if ($isFirst === true) { 333 | $requiredSpacing = $this->spacingBeforeFirst; 334 | $errorCode = 'BeforeFirst'; 335 | 336 | $phpcsFile->recordMetric($stackPtr, 'Function spacing before first', $foundLines); 337 | } else { 338 | $phpcsFile->recordMetric($stackPtr, 'Function spacing before', $foundLines); 339 | } 340 | 341 | if ($foundLines !== $requiredSpacing) { 342 | $error = 'Expected %s blank line'; 343 | if ($requiredSpacing !== 1) { 344 | $error .= 's'; 345 | } 346 | 347 | $error .= ' before function; %s found'; 348 | $data = [ 349 | $requiredSpacing, 350 | $foundLines, 351 | ]; 352 | 353 | $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data); 354 | if ($fix === true) { 355 | $nextSpace = $phpcsFile->findNext(T_WHITESPACE, ($prevContent + 1), $stackPtr); 356 | if ($nextSpace === false) { 357 | $nextSpace = ($stackPtr - 1); 358 | } 359 | 360 | if ($foundLines < $requiredSpacing) { 361 | $padding = str_repeat($phpcsFile->eolChar, ($requiredSpacing - $foundLines)); 362 | $phpcsFile->fixer->addContent($prevLineToken, $padding); 363 | } else { 364 | $nextContent = $phpcsFile->findNext(T_WHITESPACE, ($nextSpace + 1), null, true); 365 | $phpcsFile->fixer->beginChangeset(); 366 | for ($i = $nextSpace; $i < $nextContent; $i++) { 367 | if ($tokens[$i]['line'] === $tokens[$prevContent]['line']) { 368 | continue; 369 | } 370 | 371 | if ($tokens[$i]['line'] === $tokens[$nextContent]['line']) { 372 | $phpcsFile->fixer->addContentBefore($i, str_repeat($phpcsFile->eolChar, $requiredSpacing)); 373 | break; 374 | } 375 | 376 | $phpcsFile->fixer->replaceToken($i, ''); 377 | } 378 | 379 | $phpcsFile->fixer->endChangeset(); 380 | }//end if 381 | }//end if 382 | }//end if 383 | 384 | }//end process() 385 | 386 | 387 | }//end class 388 | -------------------------------------------------------------------------------- /src/NetteCodingStandard/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Nette Coding Standard 4 | --------------------------------------------------------------------------------