Skip to content

Commit 8d969b7

Browse files
committed
ClassStructureSniff fix
1 parent 5a9b3bf commit 8d969b7

File tree

1 file changed

+139
-86
lines changed

1 file changed

+139
-86
lines changed

InfinityloopCodingStandard/Sniffs/Classes/ClassStructureSniff.php

Lines changed: 139 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,7 @@ class ClassStructureSniff implements Sniff
137137
self::STAGE_MAGIC_METHODS => 200,
138138
];
139139

140-
/**
141-
* @return int[]
142-
*/
140+
/** @return array<int|string, int|string> */
143141
public function register() : array
144142
{
145143
return Tokens::$ooScopeTokens;
@@ -148,22 +146,78 @@ public function register() : array
148146
/**
149147
* @param int $pointer
150148
*
151-
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
149+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
152150
*/
153151
public function process(File $file, $pointer) : int
154152
{
155153
$tokens = $file->getTokens();
156154
$rootScopeToken = $tokens[$pointer];
157-
$rootScopeOpenerPointer = $rootScopeToken['scope_opener'];
158-
$rootScopeCloserPointer = $rootScopeToken['scope_closer'];
159155

156+
$stageLastMemberPointer = $rootScopeToken['scope_opener'];
157+
$expectedStage = self::STAGE_NONE;
158+
$stagesFirstMembers = [];
159+
while (true) {
160+
$nextStage = $this->findNextStage($file, $stageLastMemberPointer, $rootScopeToken);
161+
if ($nextStage === null) {
162+
break;
163+
}
164+
165+
[$stageFirstMemberPointer, $stageLastMemberPointer, $stage] = $nextStage;
166+
167+
if ($this->requiredOrder[$stage] >= $this->requiredOrder[$expectedStage]) {
168+
$stagesFirstMembers[$stage] = $stageFirstMemberPointer;
169+
$expectedStage = $stage;
170+
171+
continue;
172+
}
173+
174+
$fix = $file->addFixableError(
175+
sprintf('The placement of %s stage is invalid.', self::STAGE_TOKEN_NAMES[$stage]),
176+
$stageFirstMemberPointer,
177+
self::CODE_INVALID_STAGE_PLACEMENT
178+
);
179+
if (! $fix) {
180+
continue;
181+
}
182+
183+
foreach ($stagesFirstMembers as $memberStage => $firstMemberPointer) {
184+
if ($this->requiredOrder[$memberStage] <= $this->requiredOrder[$stage]) {
185+
continue;
186+
}
187+
188+
$this->fixInvalidStagePlacement(
189+
$file,
190+
$stageFirstMemberPointer,
191+
$stageLastMemberPointer,
192+
$firstMemberPointer
193+
);
194+
195+
return $pointer - 1; // run the sniff again to fix the rest of the stages
196+
}
197+
}
198+
199+
return $pointer + 1;
200+
}
201+
202+
/**
203+
* @param mixed[] $rootScopeToken
204+
*
205+
* @return array{int, int, int}|null
206+
*/
207+
private function findNextStage(File $file, int $pointer, array $rootScopeToken) : ?array
208+
{
209+
$tokens = $file->getTokens();
160210
$stageTokenTypes = [T_USE, T_CONST, T_VAR, T_STATIC, T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FUNCTION];
161211

162-
$currentTokenPointer = $rootScopeOpenerPointer + 1;
163-
$expectedStage = self::STAGE_NONE;
164-
do {
165-
$currentTokenPointer = $file->findNext($stageTokenTypes, $currentTokenPointer, $rootScopeCloserPointer);
166-
if (is_bool($currentTokenPointer)) {
212+
$currentTokenPointer = $pointer;
213+
while (true) {
214+
$currentTokenPointer = TokenHelper::findNext(
215+
$file,
216+
$stageTokenTypes,
217+
$currentToken['scope_closer'] ?? $currentTokenPointer + 1,
218+
$rootScopeToken['scope_closer']
219+
);
220+
if ($currentTokenPointer === null) {
167221
break;
168222
}
169223

@@ -173,27 +227,30 @@ public function process(File $file, $pointer) : int
173227
}
174228

175229
$stage = $this->getStageForToken($file, $currentTokenPointer);
176-
if ($stage !== null) {
177-
if ($this->requiredOrder[$stage] < $this->requiredOrder[$expectedStage]) {
178-
$fix = $file->addFixableError(
179-
sprintf('The placement of %s is invalid.', self::STAGE_TOKEN_NAMES[$stage]),
180-
$currentTokenPointer,
181-
self::CODE_INVALID_MEMBER_PLACEMENT
182-
);
183-
if ($fix) {
184-
$this->fixInvalidMemberPlacement($file, $currentTokenPointer);
185-
186-
return $pointer - 1; // run the sniff again to fix members one by one
187-
}
188-
} elseif ($this->requiredOrder[$stage] > $this->requiredOrder[$expectedStage]) {
189-
$expectedStage = $stage;
190-
}
230+
if ($stage === null) {
231+
continue;
191232
}
192233

193-
$currentTokenPointer = $currentToken['scope_closer'] ?? $currentTokenPointer + 1;
194-
} while ($currentTokenPointer !== false);
234+
if (! isset($currentStage)) {
235+
$currentStage = $stage;
236+
$stageFirstMemberPointer = $currentTokenPointer;
237+
}
195238

196-
return $pointer + 1;
239+
if ($stage !== $currentStage) {
240+
break;
241+
}
242+
243+
$stageLastMemberPointer = $currentTokenPointer;
244+
}
245+
246+
if (! isset($currentStage)) {
247+
return null;
248+
}
249+
250+
assert(isset($stageFirstMemberPointer) === true);
251+
assert(isset($stageLastMemberPointer) === true);
252+
253+
return [$stageFirstMemberPointer, $stageLastMemberPointer, $currentStage];
197254
}
198255

199256
private function getStageForToken(File $file, int $pointer) : ?int
@@ -213,6 +270,7 @@ private function getStageForToken(File $file, int $pointer) : ?int
213270
return self::STAGE_PRIVATE_CONSTANTS;
214271
}
215272

273+
// https://github.com/squizlabs/PHP_CodeSniffer/issues/2800
216274
case T_FUNCTION:
217275
$name = strtolower($tokens[$file->findNext(T_STRING, $pointer + 1)]['content']);
218276
if (array_key_exists($name, self::SPECIAL_METHODS)) {
@@ -233,6 +291,7 @@ private function getStageForToken(File $file, int $pointer) : ?int
233291
return $isStatic ? self::STAGE_PRIVATE_STATIC_METHODS : self::STAGE_PRIVATE_METHODS;
234292
}
235293

294+
// https://github.com/squizlabs/PHP_CodeSniffer/issues/2800
236295
default:
237296
$nextPointer = TokenHelper::findNextEffective($file, $pointer + 1);
238297
if ($tokens[$nextPointer]['code'] !== T_VARIABLE) {
@@ -270,7 +329,7 @@ private function getVisibilityForToken(File $file, int $pointer) : int
270329
$visibility = $tokens[$prevPointer]['code'];
271330
}
272331

273-
return $visibility ?? T_PUBLIC;
332+
return (int) ($visibility ?? T_PUBLIC);
274333
}
275334

276335
private function isMemberStatic(File $file, int $pointer) : bool
@@ -284,7 +343,7 @@ private function isMemberStatic(File $file, int $pointer) : bool
284343

285344
private function isStaticConstructor(File $file, int $pointer, bool $isStatic) : bool
286345
{
287-
if (!$isStatic) {
346+
if (! $isStatic) {
288347
return false;
289348
}
290349

@@ -317,91 +376,85 @@ private function getParentClassName(File $file, int $pointer) : string
317376
return ClassHelper::getName($file, $classPointer);
318377
}
319378

320-
private function fixInvalidMemberPlacement(File $file, int $pointer) : void
321-
{
379+
private function fixInvalidStagePlacement(
380+
File $file,
381+
int $stageFirstMemberPointer,
382+
int $stageLastMemberPointer,
383+
int $nextStageMemberPointer
384+
) : void {
322385
$tokens = $file->getTokens();
323-
$endTypes = [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON];
324-
$previousMemberEndPointer = TokenHelper::findPrevious($file, $endTypes, $pointer - 1);
325-
if ($previousMemberEndPointer === null) {
326-
throw new RuntimeException('Previous member end pointer not found');
327-
}
328-
329-
$startPointer = $this->findMemberLineStartPointer($file, $pointer, $previousMemberEndPointer);
330-
331-
if ($tokens[$pointer]['code'] === T_FUNCTION && ! FunctionHelper::isAbstract($file, $pointer)) {
332-
$endPointer = $tokens[$pointer]['scope_closer'];
333-
} else {
334-
$endPointer = TokenHelper::findNext($file, T_SEMICOLON, $pointer + 1);
335-
if ($endPointer === null) {
336-
throw new RuntimeException('End pointer not found');
337-
}
338-
}
339386

340-
$possibleWhitespaceTypes = [T_COMMENT, T_DOC_COMMENT, T_DOC_COMMENT_WHITESPACE, T_WHITESPACE];
341-
$whitespacePointer = $file->findNext($possibleWhitespaceTypes, $endPointer + 1, null, false, "\n");
342-
$nextEffectivePointer = TokenHelper::findNextEffective($file, $endPointer + 1);
343-
if ($whitespacePointer < $nextEffectivePointer) {
344-
$endPointer = $whitespacePointer;
345-
}
346-
347-
if ($tokens[$previousMemberEndPointer]['code'] === T_CLOSE_CURLY_BRACKET) {
348-
$previousScopeOpenerPointer = $tokens[$previousMemberEndPointer]['scope_opener'];
349-
$prePreviousMemberEndPointer = TokenHelper::findPrevious($file, $endTypes, $previousScopeOpenerPointer - 1);
350-
} else {
351-
$prePreviousMemberEndPointer = TokenHelper::findPrevious($file, $endTypes, $previousMemberEndPointer - 1);
352-
}
387+
$previousMemberEndPointer = $this->findPreviousMemberEndPointer($file, $stageFirstMemberPointer);
353388

354-
if ($prePreviousMemberEndPointer === null) {
355-
throw new RuntimeException('Pre-previous member end pointer not found');
356-
}
389+
$stageStartPointer = $this->findStageStartPointer($file, $stageFirstMemberPointer, $previousMemberEndPointer);
390+
$stageEndPointer = $this->findStageEndPointer($file, $stageLastMemberPointer);
357391

358-
$previousMemberStartPointer = TokenHelper::findNextEffective($file, $prePreviousMemberEndPointer + 1);
359-
if ($previousMemberStartPointer === null) {
360-
throw new RuntimeException('Previous member start pointer not found');
361-
}
392+
$nextStageMemberStartPointer = $this->findStageStartPointer($file, $nextStageMemberPointer);
362393

363-
$previousMemberStartPointer = $this->findMemberLineStartPointer(
364-
$file,
365-
$previousMemberStartPointer,
366-
$prePreviousMemberEndPointer
367-
);
368-
369-
$linesBetween = (int) $tokens[$startPointer]['line'] - (int) $tokens[$previousMemberEndPointer]['line'] - 1;
394+
$linesBetween = $tokens[$stageStartPointer]['line'] - $tokens[$previousMemberEndPointer]['line'];
370395

371396
$file->fixer->beginChangeset();
372397

373398
$content = '';
374-
for ($i = $startPointer; $i <= $endPointer; $i++) {
399+
for ($i = $stageStartPointer; $i <= $stageEndPointer; $i++) {
375400
$content .= $tokens[$i]['content'];
376401
$file->fixer->replaceToken($i, '');
377402
}
378403

379-
$this->removeBlankLinesAfterMember($file, $linesBetween, $previousMemberEndPointer, $startPointer);
404+
$this->removeBlankLinesAfterMember($file, $linesBetween, $previousMemberEndPointer, $stageStartPointer);
380405

381406
$newLines = str_repeat($file->eolChar, $linesBetween);
382-
$file->fixer->addContentBefore($previousMemberStartPointer, $content . $newLines);
407+
$file->fixer->addContentBefore($nextStageMemberStartPointer, $content . $newLines);
383408

384409
$file->fixer->endChangeset();
385410
}
386411

387-
private function findMemberLineStartPointer(
388-
File $file,
389-
int $memberStartPointer,
390-
int $previousMemberEndPointer
391-
) : int {
392-
$types = [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON];
412+
private function findPreviousMemberEndPointer(File $file, int $memberPointer) : int
413+
{
414+
$endTypes = [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON];
415+
$previousMemberEndPointer = TokenHelper::findPrevious($file, $endTypes, $memberPointer - 1);
416+
if ($previousMemberEndPointer === null) {
417+
throw new RuntimeException('Previous member end pointer not found');
418+
}
393419

394-
$startPointer = DocCommentHelper::findDocCommentOpenToken($file, $memberStartPointer);
420+
return $previousMemberEndPointer;
421+
}
422+
423+
private function findStageStartPointer(File $file, int $memberPointer, ?int $previousMemberEndPointer = null) : int
424+
{
425+
$startPointer = DocCommentHelper::findDocCommentOpenToken($file, $memberPointer - 1);
395426
if ($startPointer === null) {
427+
if ($previousMemberEndPointer === null) {
428+
$previousMemberEndPointer = $this->findPreviousMemberEndPointer($file, $memberPointer);
429+
}
430+
396431
$startPointer = TokenHelper::findNextEffective($file, $previousMemberEndPointer + 1);
397432
if ($startPointer === null) {
398433
throw new RuntimeException('Start pointer not found');
399434
}
400435
}
401436

437+
$types = [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, T_SEMICOLON];
438+
402439
return (int) $file->findFirstOnLine($types, $startPointer, true);
403440
}
404441

442+
private function findStageEndPointer(File $file, int $memberPointer) : int
443+
{
444+
$tokens = $file->getTokens();
445+
446+
if ($tokens[$memberPointer]['code'] === T_FUNCTION && ! FunctionHelper::isAbstract($file, $memberPointer)) {
447+
$endPointer = $tokens[$memberPointer]['scope_closer'];
448+
} else {
449+
$endPointer = TokenHelper::findNext($file, T_SEMICOLON, $memberPointer + 1);
450+
if ($endPointer === null) {
451+
throw new RuntimeException('End pointer not found');
452+
}
453+
}
454+
455+
return $endPointer;
456+
}
457+
405458
private function removeBlankLinesAfterMember(
406459
File $file,
407460
int $linesToRemove,

0 commit comments

Comments
 (0)