├── 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 | [](https://packagist.org/packages/nette/coding-standard)
4 | [](https://github.com/nette/coding-standard/releases)
5 | [](/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 |
--------------------------------------------------------------------------------