Skip to content

Commit 72be374

Browse files
committed
Core: properly check formatting of function declaration statements
As things were, the `WordPress.WhiteSpace.ControlStructureSpacing` sniff was being _abused_ to also check the formatting of function declaration statements, while it really isn't suitable for this. There have been numerous issues regarding this and various "plaster on the wound" fixes have been made previously. This PR is intended to fix the problem once and for all in a more proper fashion. The checks which were previously done by the `WordPress.WhiteSpace.ControlStructureSpacing` sniff consisted of the following: * Exactly one space after the `function` keyword (configurable for closures). * No space between the function _name_ and the open parenthesis. * Exactly one space after the open parenthesis. * Exactly one space before the close parenthesis. * Exactly one space before the open brace/after the close parenthesis. The above didn't take return type declarations into account properly, would run into problems with function declarations which didn't have an open brace, such as `abstract` functions or interface functions (also see 2204), and handling multi-line function declarations was undefined territory. It also led to: * Duplicate messages - the `ControlStructureSpacing` sniff reporting things also reported by the `Squiz.Functions.FunctionDeclarationArgumentSpacing` sniff or by the `Generic.WhiteSpace.LanguageConstructSpacing` sniff. * Unclear messages [1] - messages were often thrown on the `function` keyword, while the problem was further down the line. * Unclear messages [2] - messages would refer to control structures instead of functions and would otherwise not always be clear. This commit: * Removes all code related to checking function declarations from the `WordPress.WhiteSpace.ControlStructureSpacing` sniff. Includes removing the tests which were specific to function declarations. This effectively reverts the function related changes in PRs 417, 432, 440, 463, 1319, 1837 in so far as those touched the `ControlStructureSpacing` sniff. * Adds the `Squiz.Functions.MultiLineFunctionDeclaration` sniff to the `Core` ruleset to do those checks instead. The change as currently proposed has been extensively researched and tested and should provide a smooth switch over. Notes about the ruleset changes: * The `Squiz.Functions.FunctionDeclarationArgumentSpacing.SpacingBeforeClose` code was previously silenced to prevent duplicate messages, but can now be re-enabled as we'll defer to that sniff for the spacing on the inside of the parentheses. * The `Squiz.Functions.MultiLineFunctionDeclaration` sniff, by default, expects the opening brace for named functions on the line _after_ the declaration and for closures, it expects the opening brace on the _same_ line as the closure declaration. WPCS expects the opening brace on the _same_ line as the function declaration in both cases and enforces this via the `Generic.Functions.OpeningFunctionBraceKernighanRitchie` sniff (which is also used by the `Squiz.Functions.MultiLineFunctionDeclaration` sniff for the closure check). The ruleset now contains three error code exclusions to bypass the "brace on new line for named functions" checks. * As the opening brace for closures is enforced to be on the same line, we can let the new Squiz sniff handle this. To that end the `checkClosures` property for the `Generic.Functions.OpeningFunctionBraceKernighanRitchie` sniff is set to `false` to prevent duplicate messages for the same. * To prevent duplicate messages about code after the open brace for named functions, the `Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace` code has to be silenced. We cannot silence the same error code for the Squiz sniff as that would also silence the notice for closures, so silencing it for the `OpeningFunctionBraceKernighanRitchie` sniff, which now only handles named functions, is the only option, but should prevent the duplicate messages. * Additionally, the `Squiz.Functions.MultiLineFunctionDeclaration.SpaceAfterUse` error code is excluded as it overlaps with the `Generic.WhiteSpace.LanguageConstructSpacing` sniff, which also checks this. Notes about changes in behaviour before/after this change: * If the open & close parentheses are not on the same line, the space on the inside of the parentheses is no longer checked (as it is seen as a multi-line function declaration for which different rules may apply). * While not commonly used in WP, for multi-line function declarations, the sniff will now enforce one parameter per line with the first parameter on the line _after_ the open parenthesis and the close parenthesis on the line after the last parameter and indented the same as the start of the function declaration statement. A function is considered multi-line when the open and close parentheses are not on the same line, which means that for closures with `use` statements, these rules will apply to both the function parameter list as well as the use parameter list. * It is no longer possible to choose the spacing between the `function` keyword for closures and the open parenthesis. This will no always be enforced to be a single space, which is in line with PSR12. A check of WP Core showed that there was no consistency at all regarding this spacing, with approximately 50/50 of closures having no space vs 1 space, so defering to PSR12 and enforcing consistency in this, seems like a reasonable choice. Other notes: * The `WordPress.WhiteSpace.ControlStructureSpacing` sniff did not support formatting checks for PHP 7.4 arrow functions. At this time, the `Squiz.Functions.MultiLineFunctionDeclaration` sniff does not do so either. There is a PR open upstream squizlabs/php_codesniffer 3661 to add support for arrow functions, though that PR still needs an update for the (very inconsistent) rule chosen by PSR-PER to [enforce _no_ space between the `fn` keyword and the open parenthesis for arrow functions](php-fig/per-coding-style#53). * There is also a PR open upstream - squizlabs/php_codesniffer 3739 - to fix two fixer issues. At this time, these do not pose a problem for WP Core as (aside from the closure keyword spacing), WP Core is "clean", but would be nice if that PR got merged to prevent future issues. * While doing the research for this PR, I discovered an issue with the `Generic.Functions.OpeningFunctionBraceKernighanRitchie` sniff. It doesn't check the spacing before the open brace for empty functions. PR squizlabs/php_codesniffer 3869 has been opened to fix that. As empty functions are the exception, not the rule, I'm hoping it will get merged before any issues about this are reported. Fixes 1101 Note: check 8 identified in issue 1101 is still open, but there is a separate issue open to discuss that in more depth - 1474 - and that issue remains open (for now).
1 parent efbaa1d commit 72be374

File tree

5 files changed

+47
-422
lines changed

5 files changed

+47
-422
lines changed

WordPress-Core/ruleset.xml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,19 +199,37 @@
199199
<!-- Covers rule: Define a function like so: function my_function( $param1 = 'foo', $param2 = 'bar' ) { -->
200200
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie">
201201
<properties>
202-
<property name="checkClosures" value="true"/>
202+
<property name="checkClosures" value="false"/>
203203
</properties>
204204
</rule>
205+
<!-- Prevent duplicate message. This is now checked by the Squiz.Functions.MultiLineFunctionDeclaration sniff. -->
206+
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace">
207+
<severity>0</severity>
208+
</rule>
209+
210+
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration"/>
211+
<!-- WP demands brace on same line, not on the next line. Silence errors related to "brace on same line". -->
212+
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine">
213+
<severity>0</severity>
214+
</rule>
215+
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceSpacing">
216+
<severity>0</severity>
217+
</rule>
218+
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceIndent">
219+
<severity>0</severity>
220+
</rule>
221+
<!-- Prevent duplicate message. This is already checked by the Generic.WhiteSpace.LanguageConstructSpacing sniff. -->
222+
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.SpaceAfterUse">
223+
<severity>0</severity>
224+
</rule>
225+
205226
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
206227
<properties>
207228
<property name="equalsSpacing" value="1"/>
208229
<property name="requiredSpacesAfterOpen" value="1"/>
209230
<property name="requiredSpacesBeforeClose" value="1"/>
210231
</properties>
211232
</rule>
212-
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.SpacingBeforeClose">
213-
<severity>0</severity>
214-
</rule>
215233
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.SpacingAfterVariadic">
216234
<severity>0</severity>
217235
</rule>

WordPress/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php

Lines changed: 14 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
use WordPressCS\WordPress\Sniff;
1313
use PHP_CodeSniffer\Util\Tokens;
14-
use PHPCSUtils\Utils\UseStatements;
1514

1615
/**
1716
* Enforces spacing around logical operators and assignments, based upon Squiz code.
@@ -23,6 +22,7 @@
2322
* @since 0.3.0 This sniff now has the ability to fix most errors it flags.
2423
* @since 0.7.0 This class now extends the WordPressCS native `Sniff` class.
2524
* @since 0.13.0 Class name changed: this class is now namespaced.
25+
* @since 3.0.0 Checks related to function declarations have been removed from this sniff.
2626
*
2727
* Last synced with base class 2017-01-15 at commit b024ad84656c37ef5733c6998ebc1e60957b2277.
2828
* Note: This class has diverged quite far from the original. All the same, checking occasionally
@@ -52,33 +52,19 @@ final class ControlStructureSpacingSniff extends Sniff {
5252
*/
5353
public $space_before_colon = 'required';
5454

55-
/**
56-
* How many spaces should be between a T_CLOSURE and T_OPEN_PARENTHESIS.
57-
*
58-
* `function[*]() {...}`
59-
*
60-
* @since 0.7.0
61-
*
62-
* @var int
63-
*/
64-
public $spaces_before_closure_open_paren = -1;
65-
6655
/**
6756
* Tokens for which to ignore extra space on the inside of parenthesis.
6857
*
69-
* For functions, this is already checked by the Squiz.Functions.FunctionDeclarationArgumentSpacing sniff.
7058
* For do / else / try, there are no parenthesis, so skip it.
7159
*
7260
* @since 0.11.0
7361
*
7462
* @var array
7563
*/
7664
private $ignore_extra_space_after_open_paren = array(
77-
\T_FUNCTION => true,
78-
\T_CLOSURE => true,
79-
\T_DO => true,
80-
\T_ELSE => true,
81-
\T_TRY => true,
65+
\T_DO => true,
66+
\T_ELSE => true,
67+
\T_TRY => true,
8268
);
8369

8470
/**
@@ -96,9 +82,6 @@ public function register() {
9682
\T_DO,
9783
\T_ELSE,
9884
\T_ELSEIF,
99-
\T_FUNCTION,
100-
\T_CLOSURE,
101-
\T_USE,
10285
\T_TRY,
10386
\T_CATCH,
10487
);
@@ -112,18 +95,8 @@ public function register() {
11295
* @return void
11396
*/
11497
public function process_token( $stackPtr ) {
115-
$this->spaces_before_closure_open_paren = (int) $this->spaces_before_closure_open_paren;
116-
117-
if ( \T_USE === $this->tokens[ $stackPtr ]['code']
118-
&& UseStatements::isClosureUse( $this->phpcsFile, $stackPtr ) === false
119-
) {
120-
return;
121-
}
122-
12398
if ( isset( $this->tokens[ ( $stackPtr + 1 ) ] ) && \T_WHITESPACE !== $this->tokens[ ( $stackPtr + 1 ) ]['code']
12499
&& ! ( \T_ELSE === $this->tokens[ $stackPtr ]['code'] && \T_COLON === $this->tokens[ ( $stackPtr + 1 ) ]['code'] )
125-
&& ! ( \T_CLOSURE === $this->tokens[ $stackPtr ]['code']
126-
&& 0 >= $this->spaces_before_closure_open_paren )
127100
) {
128101
$error = 'Space after opening control structure is required';
129102
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceAfterStructureOpen' );
@@ -133,12 +106,8 @@ public function process_token( $stackPtr ) {
133106
}
134107
}
135108

136-
if ( ! isset( $this->tokens[ $stackPtr ]['scope_closer'] ) ) {
137-
138-
if ( \T_USE === $this->tokens[ $stackPtr ]['code'] ) {
139-
$scopeOpener = $this->phpcsFile->findNext( \T_OPEN_CURLY_BRACKET, ( $stackPtr + 1 ) );
140-
$scopeCloser = $this->tokens[ $scopeOpener ]['scope_closer'];
141-
} elseif ( \T_WHILE !== $this->tokens[ $stackPtr ]['code'] ) {
109+
if ( ! isset( $this->tokens[ $stackPtr ]['scope_opener'], $this->tokens[ $stackPtr ]['scope_closer'] ) ) {
110+
if ( \T_WHILE !== $this->tokens[ $stackPtr ]['code'] ) {
142111
return;
143112
}
144113
} else {
@@ -174,102 +143,15 @@ public function process_token( $stackPtr ) {
174143

175144
$parenthesisOpener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
176145

177-
// If this is a function declaration.
178-
if ( \T_FUNCTION === $this->tokens[ $stackPtr ]['code'] ) {
179-
180-
if ( \T_STRING === $this->tokens[ $parenthesisOpener ]['code'] ) {
181-
182-
$function_name_ptr = $parenthesisOpener;
183-
184-
} elseif ( \T_BITWISE_AND === $this->tokens[ $parenthesisOpener ]['code'] ) {
185-
186-
// This function returns by reference, i.e. `function &function_name() {}`.
187-
$parenthesisOpener = $this->phpcsFile->findNext(
188-
Tokens::$emptyTokens,
189-
( $parenthesisOpener + 1 ),
190-
null,
191-
true
192-
);
193-
$function_name_ptr = $parenthesisOpener;
194-
}
195-
196-
if ( isset( $function_name_ptr ) ) {
197-
$parenthesisOpener = $this->phpcsFile->findNext(
198-
Tokens::$emptyTokens,
199-
( $parenthesisOpener + 1 ),
200-
null,
201-
true
202-
);
203-
204-
// Checking space between name and open parentheses, i.e. `function my_function[*](...) {}`.
205-
if ( ( $function_name_ptr + 1 ) !== $parenthesisOpener ) {
206-
207-
$error = 'Space between function name and opening parenthesis is prohibited.';
208-
$fix = $this->phpcsFile->addFixableError(
209-
$error,
210-
$stackPtr,
211-
'SpaceBeforeFunctionOpenParenthesis',
212-
array( $this->tokens[ ( $function_name_ptr + 1 ) ]['content'] )
213-
);
214-
215-
if ( true === $fix ) {
216-
$this->phpcsFile->fixer->replaceToken( ( $function_name_ptr + 1 ), '' );
217-
}
218-
}
219-
}
220-
} elseif ( \T_CLOSURE === $this->tokens[ $stackPtr ]['code'] ) {
221-
222-
// Check if there is a use () statement.
223-
if ( isset( $this->tokens[ $parenthesisOpener ]['parenthesis_closer'] ) ) {
224-
225-
$usePtr = $this->phpcsFile->findNext(
226-
Tokens::$emptyTokens,
227-
( $this->tokens[ $parenthesisOpener ]['parenthesis_closer'] + 1 ),
228-
null,
229-
true,
230-
null,
231-
true
232-
);
233-
234-
// If it is, we set that as the "scope opener".
235-
if ( \T_USE === $this->tokens[ $usePtr ]['code'] ) {
236-
$scopeOpener = $usePtr;
237-
}
238-
}
239-
}
240-
241146
if ( \T_COLON !== $this->tokens[ $parenthesisOpener ]['code']
242-
&& \T_FUNCTION !== $this->tokens[ $stackPtr ]['code']
147+
&& ( $stackPtr + 1 ) === $parenthesisOpener
243148
) {
149+
// Checking space between keyword and open parenthesis, i.e. `if[*](...) {}`.
150+
$error = 'No space before opening parenthesis is prohibited';
151+
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceBeforeOpenParenthesis' );
244152

245-
if ( \T_CLOSURE === $this->tokens[ $stackPtr ]['code']
246-
&& 0 === $this->spaces_before_closure_open_paren
247-
) {
248-
249-
if ( ( $stackPtr + 1 ) !== $parenthesisOpener ) {
250-
// Checking space between keyword and open parenthesis, i.e. `function[*](...) {}`.
251-
$error = 'Space before closure opening parenthesis is prohibited';
252-
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'SpaceBeforeClosureOpenParenthesis' );
253-
254-
if ( true === $fix ) {
255-
$this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), '' );
256-
}
257-
}
258-
} elseif (
259-
(
260-
\T_CLOSURE !== $this->tokens[ $stackPtr ]['code']
261-
|| 1 === $this->spaces_before_closure_open_paren
262-
)
263-
&& ( $stackPtr + 1 ) === $parenthesisOpener
264-
) {
265-
266-
// Checking space between keyword and open parenthesis, i.e. `if[*](...) {}`.
267-
$error = 'No space before opening parenthesis is prohibited';
268-
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceBeforeOpenParenthesis' );
269-
270-
if ( true === $fix ) {
271-
$this->phpcsFile->fixer->addContent( $stackPtr, ' ' );
272-
}
153+
if ( true === $fix ) {
154+
$this->phpcsFile->fixer->addContent( $stackPtr, ' ' );
273155
}
274156
}
275157

@@ -292,7 +174,7 @@ public function process_token( $stackPtr ) {
292174

293175
if ( \T_CLOSE_PARENTHESIS !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['code'] ) {
294176
if ( \T_WHITESPACE !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['code'] ) {
295-
// Checking space directly after the open parenthesis, i.e. `$value = my_function([*]...)`.
177+
// Checking space directly after the open parenthesis, i.e. `if ([*]...) {}`.
296178
$error = 'No space after opening parenthesis is prohibited';
297179
$fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceAfterOpenParenthesis' );
298180

@@ -351,11 +233,6 @@ public function process_token( $stackPtr ) {
351233
}
352234

353235
if ( \T_WHITESPACE !== $this->tokens[ ( $parenthesisCloser + 1 ) ]['code']
354-
&& ! ( // Do NOT flag : immediately following ) for return types declarations.
355-
\T_COLON === $this->tokens[ ( $parenthesisCloser + 1 ) ]['code']
356-
&& ( isset( $this->tokens[ $parenthesisCloser ]['parenthesis_owner'] ) === false
357-
|| in_array( $this->tokens[ $this->tokens[ $parenthesisCloser ]['parenthesis_owner'] ]['code'], array( \T_FUNCTION, \T_CLOSURE ), true ) )
358-
)
359236
&& ( isset( $scopeOpener ) && \T_COLON !== $this->tokens[ $scopeOpener ]['code'] )
360237
) {
361238
$error = 'Space between opening control structure and closing parenthesis is required';
@@ -367,10 +244,7 @@ public function process_token( $stackPtr ) {
367244
}
368245
}
369246

370-
// Ignore this for function declarations. Handled by the OpeningFunctionBraceKernighanRitchie sniff.
371-
if ( \T_FUNCTION !== $this->tokens[ $stackPtr ]['code']
372-
&& \T_CLOSURE !== $this->tokens[ $stackPtr ]['code']
373-
&& isset( $this->tokens[ $parenthesisOpener ]['parenthesis_owner'] )
247+
if ( isset( $this->tokens[ $parenthesisOpener ]['parenthesis_owner'] )
374248
&& ( isset( $scopeOpener )
375249
&& $this->tokens[ $parenthesisCloser ]['line'] !== $this->tokens[ $scopeOpener ]['line'] )
376250
) {
@@ -392,7 +266,6 @@ public function process_token( $stackPtr ) {
392266
} elseif ( \T_WHITESPACE === $this->tokens[ ( $parenthesisCloser + 1 ) ]['code']
393267
&& ' ' !== $this->tokens[ ( $parenthesisCloser + 1 ) ]['content']
394268
) {
395-
396269
// Checking space between the close parenthesis and the open brace, i.e. `if (...) [*]{}`.
397270
$error = 'Expected exactly one space between closing parenthesis and opening control structure; "%s" found.';
398271
$fix = $this->phpcsFile->addFixableError(

0 commit comments

Comments
 (0)