Skip to content

Commit 0d57749

Browse files
authored
Merge pull request #65 from PHPCSStandards/feature/new-spacesfixer-class
New Fixers\SpacesFixer class
2 parents 02db423 + aa30b05 commit 0d57749

15 files changed

+1363
-0
lines changed

PHPCSUtils/Fixers/SpacesFixer.php

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<?php
2+
/**
3+
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4+
*
5+
* @package PHPCSUtils
6+
* @copyright 2019 PHPCSUtils Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSUtils
9+
*/
10+
11+
namespace PHPCSUtils\Fixers;
12+
13+
use PHP_CodeSniffer\Exceptions\RuntimeException;
14+
use PHP_CodeSniffer\Files\File;
15+
use PHP_CodeSniffer\Util\Tokens;
16+
use PHPCSUtils\Utils\Numbers;
17+
18+
/**
19+
* Utility to check and, if necessary, fix the whitespace between two tokens.
20+
*
21+
* @since 1.0.0
22+
*/
23+
class SpacesFixer
24+
{
25+
26+
/**
27+
* Check the whitespace between two tokens, throw an error if it doesn't match the
28+
* expected whitespace and if relevant, fix it.
29+
*
30+
* Note:
31+
* - This method will not auto-fix if there is anything but whitespace between the two
32+
* tokens. In that case, it will throw a non-fixable error/warning.
33+
* - If 'newline' is expected and _no_ new line is encountered, a new line will be added,
34+
* but no assumptions will be made about the intended indentation of the code.
35+
* This should be handled by a (separate) indentation sniff.
36+
* - If 'newline' is expected and multiple new lines are encountered, this will be accepted
37+
* as valid.
38+
* No assumptions are made about whether additional blank lines are allowed or not.
39+
* If _exactly_ one line is desired, combine this Fixer with the BlankLineFixer.
40+
* - The fixer will not leave behind any trailing spaces on the original line when fixing
41+
* to 'newline', but it will not correct _existing_ trailing spaces when there already
42+
* is a new line in place.
43+
* - This method can optionally record a metric for this check which will be displayed
44+
* when the end-user requests the `info` report.
45+
*
46+
* @since 1.0.0
47+
*
48+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
49+
* @param int $stackPtr The position of the token which should be used
50+
* when reporting an issue.
51+
* @param int $secondPtr The stack pointer to the second token.
52+
* This token can be before or after the $stackPtr,
53+
* but should only be seperated from the $stackPtr
54+
* by whitespace and/or comments/annotations.
55+
* @param string|int $expectedSpaces Number of spaces to enforce.
56+
* Valid values:
57+
* - (int) Number of spaces. Must be 0 or more.
58+
* - (string) 'newline'.
59+
* @param string $errorTemplate Error message template. This string should contain
60+
* two placeholders:
61+
* %1$s = expected spaces phrase.
62+
* %2$s = found spaces phrase.
63+
* Note: _The replacement phrase will be in human
64+
* readable English and include "spaces"/"new line",
65+
* so no need to include that in the template._
66+
* @param string $errorCode A violation code unique to the sniff message.
67+
* Defaults to `Found`.
68+
* It is strongly recommended to change this if
69+
* this fixer is used for different errors in the
70+
* same sniff.
71+
* @param string $errorType Optional. Whether to report the issue as a
72+
* `warning` or an `error`. Defaults to `error`.
73+
* @param string $errorSeverity Optional. The severity level for this message.
74+
* A value of 0 will be converted into the default
75+
* severity level.
76+
* @param string $metricName Optional. The name of the metric to record.
77+
* This can be a short description phrase.
78+
* Leave empty to not record metrics.
79+
*
80+
* @return void
81+
*
82+
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the tokens passed do not exist or are whitespace
83+
* tokens.
84+
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If $expectedSpaces is not a valid value.
85+
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the tokens passed are separated by more than just
86+
* empty (whitespace + comments/annotations) tokens.
87+
*/
88+
public static function checkAndFix(
89+
File $phpcsFile,
90+
$stackPtr,
91+
$secondPtr,
92+
$expectedSpaces,
93+
$errorTemplate,
94+
$errorCode = 'Found',
95+
$errorType = 'error',
96+
$errorSeverity = 0,
97+
$metricName = ''
98+
) {
99+
$tokens = $phpcsFile->getTokens();
100+
101+
/*
102+
* Validate the received function input.
103+
*/
104+
105+
if (isset($tokens[$stackPtr], $tokens[$secondPtr]) === false
106+
|| $tokens[$stackPtr]['code'] === \T_WHITESPACE
107+
|| $tokens[$secondPtr]['code'] === \T_WHITESPACE
108+
) {
109+
throw new RuntimeException('The $stackPtr and the $secondPtr token must exist and not be whitespace');
110+
}
111+
112+
$expected = false;
113+
if ($expectedSpaces === 'newline') {
114+
$expected = $expectedSpaces;
115+
} elseif (\is_int($expectedSpaces) === true && $expectedSpaces >= 0) {
116+
$expected = $expectedSpaces;
117+
} elseif (\is_string($expectedSpaces) === true && Numbers::isDecimalInt($expectedSpaces) === true) {
118+
$expected = (int) $expectedSpaces;
119+
}
120+
121+
if ($expected === false) {
122+
throw new RuntimeException(
123+
'The $expectedSpaces setting should be either "newline", 0 or a positive integer'
124+
);
125+
}
126+
127+
$ptrA = $stackPtr;
128+
$ptrB = $secondPtr;
129+
if ($stackPtr > $secondPtr) {
130+
$ptrA = $secondPtr;
131+
$ptrB = $stackPtr;
132+
}
133+
134+
$nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($ptrA + 1), null, true);
135+
if ($nextNonEmpty < $ptrB) {
136+
throw new RuntimeException(
137+
'The $stackPtr and the $secondPtr token must be adjacent tokens separated only'
138+
. ' by whitespace and/or comments'
139+
);
140+
}
141+
142+
/*
143+
* Determine how many spaces are between the two tokens.
144+
*/
145+
146+
$found = 0;
147+
$foundPhrase = 'no spaces';
148+
if (($ptrA + 1) !== $ptrB) {
149+
if ($tokens[$ptrA]['line'] !== $tokens[$ptrB]['line']) {
150+
$found = 'newline';
151+
$foundPhrase = 'a new line';
152+
if (($tokens[$ptrA]['line'] + 1) !== $tokens[$ptrB]['line']) {
153+
$foundPhrase = 'multiple new lines';
154+
}
155+
} elseif ($tokens[($ptrA + 1)]['code'] === \T_WHITESPACE) {
156+
$found = $tokens[($ptrA + 1)]['length'];
157+
$foundPhrase = $found . (($found === 1) ? ' space' : ' spaces');
158+
} else {
159+
$found = 'non-whitespace tokens';
160+
$foundPhrase = 'non-whitespace tokens';
161+
}
162+
}
163+
164+
if ($metricName !== '') {
165+
$phpcsFile->recordMetric($stackPtr, $metricName, $foundPhrase);
166+
}
167+
168+
if ($found === $expected) {
169+
return;
170+
}
171+
172+
/*
173+
* Handle the violation message.
174+
*/
175+
176+
$expectedPhrase = 'no space';
177+
if ($expected === 'newline') {
178+
$expectedPhrase = 'a new line';
179+
} elseif ($expected === 1) {
180+
$expectedPhrase = $expected . ' space';
181+
} elseif ($expected > 1) {
182+
$expectedPhrase = $expected . ' spaces';
183+
}
184+
185+
$fixable = true;
186+
$nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($ptrA + 1), null, true);
187+
if ($nextNonWhitespace !== $ptrB) {
188+
// Comment found between the tokens and we don't know where it should go, so don't auto-fix.
189+
$fixable = false;
190+
}
191+
192+
if ($found === 'newline'
193+
&& $tokens[$ptrA]['code'] === \T_COMMENT
194+
&& \substr($tokens[$ptrA]['content'], -2) !== '*/'
195+
) {
196+
/*
197+
* $ptrA is a slash-style trailing comment, removing the new line would comment out
198+
* the code, so don't auto-fix.
199+
*/
200+
$fixable = false;
201+
}
202+
203+
$method = 'add';
204+
$method .= ($fixable === true) ? 'Fixable' : '';
205+
$method .= ($errorType === 'error') ? 'Error' : 'Warning';
206+
207+
$recorded = $phpcsFile->$method(
208+
$errorTemplate,
209+
$stackPtr,
210+
$errorCode,
211+
[$expectedPhrase, $foundPhrase],
212+
$errorSeverity
213+
);
214+
215+
if ($fixable === false || $recorded === false) {
216+
return;
217+
}
218+
219+
/*
220+
* Fix the violation.
221+
*/
222+
223+
$phpcsFile->fixer->beginChangeset();
224+
225+
/*
226+
* Remove existing whitespace. No need to check if it's whitespace as otherwise the fixer
227+
* wouldn't have kicked in.
228+
*/
229+
for ($i = ($ptrA + 1); $i < $ptrB; $i++) {
230+
$phpcsFile->fixer->replaceToken($i, '');
231+
}
232+
233+
// If necessary: add the correct amount whitespace.
234+
if ($expected !== 0) {
235+
if ($expected === 'newline') {
236+
$phpcsFile->fixer->addContent($ptrA, $phpcsFile->eolChar);
237+
} else {
238+
$replacement = $tokens[$ptrA]['content'] . \str_repeat(' ', $expected);
239+
$phpcsFile->fixer->replaceToken($ptrA, $replacement);
240+
}
241+
}
242+
243+
$phpcsFile->fixer->endChangeset();
244+
}
245+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
/* testPassingWhitespace1 */
4+
echo 'foo' /* testPassingWhitespace2 */ ;
5+
6+
/* testPassingTokensWithSomethingBetween */
7+
echo 'foo' . 'bar';

0 commit comments

Comments
 (0)