@@ -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