Skip to content

Commit fed7ec9

Browse files
committed
✨ New Universal.WhiteSpace.FirstClassCallableSpacing sniff
This sniff checks the whitespace around the ellipsis in a first class callable and can be used to address the following rule from PER-CS: > **4.8 Function Callable References** > A function or method may be referenced in a way that creates a closure out of it, by providing `...` in place of arguments. > > If so, the `...` MUST NOT include any whitespace before or after. That is, the correct format is `foo(...)`. Ref: https://www.php-fig.org/per/coding-style/#48-function-callable-references The desired "spacing" before/after the ellipsis is configurable and defaults to "no space". Includes fixer. Includes unit tests. Includes documentation.
1 parent 4eeabc4 commit fed7ec9

File tree

6 files changed

+303
-0
lines changed

6 files changed

+303
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0"?>
2+
<documentation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://phpcsstandards.github.io/PHPCSDevTools/phpcsdocs.xsd"
4+
title="First Class Callable Spacing"
5+
>
6+
<standard>
7+
<![CDATA[
8+
There should be no space around the ellipsis when used in a first class callable.
9+
]]>
10+
</standard>
11+
<code_comparison>
12+
<code title="Valid: No space around the ellipsis in a first class callable.">
13+
<![CDATA[
14+
$callback = foo(<em>...</em>);
15+
array_map(
16+
$this->method(<em>...</em>),
17+
$array
18+
);
19+
]]>
20+
</code>
21+
<code title="Invalid: Space found around the ellipsis in a first class callable.">
22+
<![CDATA[
23+
$callback = foo(<em> </em>...<em> </em>);
24+
array_map(
25+
$this->method(<em>
26+
</em>...<em>
27+
</em>),
28+
$array
29+
);
30+
]]>
31+
</code>
32+
</code_comparison>
33+
</documentation>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
/**
3+
* PHPCSExtra, a collection of sniffs and standards for use with PHP_CodeSniffer.
4+
*
5+
* @package PHPCSExtra
6+
* @copyright 2020 PHPCSExtra Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSExtra
9+
*/
10+
11+
namespace PHPCSExtra\Universal\Sniffs\WhiteSpace;
12+
13+
use PHP_CodeSniffer\Files\File;
14+
use PHP_CodeSniffer\Sniffs\Sniff;
15+
use PHP_CodeSniffer\Util\Tokens;
16+
use PHPCSUtils\Fixers\SpacesFixer;
17+
18+
/**
19+
* Checks the spacing around the ellipses for first class callables.
20+
*
21+
* @since 1.5.0
22+
*/
23+
final class FirstClassCallableSpacingSniff implements Sniff
24+
{
25+
26+
/**
27+
* The number of spaces to demand before and after the ellipsis for a first class callable.
28+
*
29+
* @since 1.5.0
30+
*
31+
* @var int
32+
*/
33+
public $spacing = 0;
34+
35+
/**
36+
* Returns an array of tokens this test wants to listen for.
37+
*
38+
* @since 1.5.0
39+
*
40+
* @return array<int|string>
41+
*/
42+
public function register()
43+
{
44+
return [\T_ELLIPSIS];
45+
}
46+
47+
/**
48+
* Processes this test, when one of its tokens is encountered.
49+
*
50+
* @since 1.5.0
51+
*
52+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
53+
* @param int $stackPtr The position of the current token
54+
* in the stack passed in $tokens.
55+
*
56+
* @return void
57+
*/
58+
public function process(File $phpcsFile, $stackPtr)
59+
{
60+
$tokens = $phpcsFile->getTokens();
61+
62+
// Verify this is an ellipsis for a first class callable.
63+
$previousNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
64+
if ($tokens[$previousNonEmpty]['code'] !== \T_OPEN_PARENTHESIS) {
65+
return;
66+
}
67+
68+
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
69+
if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_CLOSE_PARENTHESIS) {
70+
return;
71+
}
72+
73+
$spacing = (int) $this->spacing;
74+
75+
// Check spacing before the ellipsis.
76+
SpacesFixer::checkAndFix(
77+
$phpcsFile,
78+
$previousNonEmpty,
79+
$stackPtr,
80+
$spacing,
81+
'Incorrect spacing between first class callable open parentheses and ellipsis. Expected: %1$s, found: %2$s.',
82+
'SpacingBefore',
83+
'error',
84+
0,
85+
'First class callables: space before ellipsis'
86+
);
87+
88+
// Check spacing after the ellipsis.
89+
SpacesFixer::checkAndFix(
90+
$phpcsFile,
91+
$stackPtr,
92+
$nextNonEmpty,
93+
$spacing,
94+
'Incorrect spacing between first class callable ellipsis and close parentheses. Expected: %1$s, found: %2$s.',
95+
'SpacingAfter',
96+
'error',
97+
0,
98+
'First class callables: space after ellipsis'
99+
);
100+
}
101+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
// Not our target...
4+
function foo(...$args) {}
5+
callMe($foo, ...$args);
6+
$array = [$a, ...$b, $c];
7+
8+
// First class callables.
9+
callMe(...); // OK.
10+
callMe( ...);
11+
callMe(... );
12+
callMe( ... );
13+
callMe( ... );
14+
callMe(
15+
16+
...
17+
18+
);
19+
callMe( /*comment*/ ... );
20+
21+
// phpcs:set Universal.WhiteSpace.FirstClassCallableSpacing spacing 1
22+
callMe( ... ); // OK.
23+
callMe(... );
24+
callMe( ...);
25+
callMe(...);
26+
callMe( ... );
27+
callMe(
28+
29+
30+
...
31+
);
32+
callMe(... /*comment*/ );
33+
34+
// phpcs:set Universal.WhiteSpace.FirstClassCallableSpacing spacing 2
35+
callMe( ... ); // OK.
36+
callMe(... );
37+
callMe( ...);
38+
callMe(...);
39+
callMe( ... );
40+
callMe( ... );
41+
callMe(
42+
...
43+
44+
45+
);
46+
callMe(... /*comment*/ );
47+
48+
// Reset the property to its default value.
49+
// phpcs:set Universal.WhiteSpace.FirstClassCallableSpacing spacing 0
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
// Not our target...
4+
function foo(...$args) {}
5+
callMe($foo, ...$args);
6+
$array = [$a, ...$b, $c];
7+
8+
// First class callables.
9+
callMe(...); // OK.
10+
callMe(...);
11+
callMe(...);
12+
callMe(...);
13+
callMe(...);
14+
callMe(...);
15+
callMe( /*comment*/ ...);
16+
17+
// phpcs:set Universal.WhiteSpace.FirstClassCallableSpacing spacing 1
18+
callMe( ... ); // OK.
19+
callMe( ... );
20+
callMe( ... );
21+
callMe( ... );
22+
callMe( ... );
23+
callMe( ... );
24+
callMe( ... /*comment*/ );
25+
26+
// phpcs:set Universal.WhiteSpace.FirstClassCallableSpacing spacing 2
27+
callMe( ... ); // OK.
28+
callMe( ... );
29+
callMe( ... );
30+
callMe( ... );
31+
callMe( ... );
32+
callMe( ... );
33+
callMe( ... );
34+
callMe( ... /*comment*/ );
35+
36+
// Reset the property to its default value.
37+
// phpcs:set Universal.WhiteSpace.FirstClassCallableSpacing spacing 0
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
// Intentional parse error.
4+
// This must be the only test in the file.
5+
// This code sniff should **not** be flagged as we cannot determine for certain whether this is a first class callable or not.
6+
callable( ...
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
/**
3+
* PHPCSExtra, a collection of sniffs and standards for use with PHP_CodeSniffer.
4+
*
5+
* @package PHPCSExtra
6+
* @copyright 2020 PHPCSExtra Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSExtra
9+
*/
10+
11+
namespace PHPCSExtra\Universal\Tests\WhiteSpace;
12+
13+
use PHP_CodeSniffer\Tests\Standards\AbstractSniffTestCase;
14+
15+
/**
16+
* Unit test class for the FirstClassCallableSpacing sniff.
17+
*
18+
* @covers PHPCSExtra\Universal\Sniffs\WhiteSpace\FirstClassCallableSpacingSniff
19+
*
20+
* @since 1.5.0
21+
*/
22+
final class FirstClassCallableSpacingUnitTest extends AbstractSniffTestCase
23+
{
24+
25+
/**
26+
* Returns the lines where errors should occur.
27+
*
28+
* @param string $testFile The name of the file being tested.
29+
*
30+
* @return array<int, int> Key is the line number, value is the number of expected errors.
31+
*/
32+
public function getErrorList($testFile = '')
33+
{
34+
switch ($testFile) {
35+
case 'FirstClassCallableSpacingUnitTest.1.inc':
36+
return [
37+
10 => 1,
38+
11 => 1,
39+
12 => 2,
40+
13 => 2,
41+
14 => 1,
42+
16 => 1,
43+
19 => 2,
44+
45+
23 => 1,
46+
24 => 1,
47+
25 => 2,
48+
26 => 2,
49+
27 => 1,
50+
30 => 1,
51+
32 => 1,
52+
53+
36 => 1,
54+
37 => 1,
55+
38 => 2,
56+
39 => 2,
57+
40 => 2,
58+
41 => 1,
59+
42 => 1,
60+
46 => 2,
61+
];
62+
63+
default:
64+
return [];
65+
}
66+
}
67+
68+
/**
69+
* Returns the lines where warnings should occur.
70+
*
71+
* @return array<int, int> Key is the line number, value is the number of expected warnings.
72+
*/
73+
public function getWarningList()
74+
{
75+
return [];
76+
}
77+
}

0 commit comments

Comments
 (0)