5
5
use PHP_CodeSniffer \Files \File ;
6
6
use PHP_CodeSniffer \Sniffs \Sniff ;
7
7
use PHP_CodeSniffer \Util \Tokens ;
8
+ use SlevomatCodingStandard \Helpers \AnnotationHelper ;
9
+ use SlevomatCodingStandard \Helpers \AttributeHelper ;
8
10
use SlevomatCodingStandard \Helpers \ClassHelper ;
9
11
use SlevomatCodingStandard \Helpers \DocCommentHelper ;
10
12
use SlevomatCodingStandard \Helpers \FixerHelper ;
11
13
use SlevomatCodingStandard \Helpers \FunctionHelper ;
14
+ use SlevomatCodingStandard \Helpers \NamespaceHelper ;
12
15
use SlevomatCodingStandard \Helpers \PropertyHelper ;
13
16
use SlevomatCodingStandard \Helpers \SniffSettingsHelper ;
14
17
use SlevomatCodingStandard \Helpers \TokenHelper ;
18
21
use function array_key_exists ;
19
22
use function array_keys ;
20
23
use function array_merge ;
24
+ use function array_shift ;
21
25
use function array_values ;
22
26
use function assert ;
23
27
use function implode ;
24
28
use function in_array ;
29
+ use function ltrim ;
25
30
use function preg_replace ;
26
31
use function preg_split ;
27
32
use function sprintf ;
28
33
use function str_repeat ;
29
34
use function strtolower ;
35
+ use function substr ;
36
+ use const PREG_SPLIT_NO_EMPTY ;
30
37
use const T_ABSTRACT ;
38
+ use const T_ATTRIBUTE_END ;
31
39
use const T_CLOSE_CURLY_BRACKET ;
32
40
use const T_CONST ;
33
41
use const T_ENUM_CASE ;
@@ -180,9 +188,15 @@ class ClassStructureSniff implements Sniff
180
188
'__debuginfo ' => self ::GROUP_MAGIC_METHODS ,
181
189
];
182
190
191
+ /** @var array<string, string> */
192
+ public $ methodGroups = [];
193
+
183
194
/** @var list<string> */
184
195
public $ groups = [];
185
196
197
+ /** @var array<string, list<array{name: string|null, attributes: array<string>, annotations: array<string>}>>|null */
198
+ private $ normalizedMethodGroups ;
199
+
186
200
/** @var array<string, int>|null */
187
201
private $ normalizedGroups ;
188
202
@@ -267,7 +281,6 @@ private function findNextGroup(File $phpcsFile, int $pointer, array $rootScopeTo
267
281
{
268
282
$ tokens = $ phpcsFile ->getTokens ();
269
283
$ groupTokenTypes = [T_USE , T_ENUM_CASE , T_CONST , T_VARIABLE , T_FUNCTION ];
270
-
271
284
$ currentTokenPointer = $ pointer ;
272
285
while (true ) {
273
286
$ currentTokenPointer = TokenHelper::findNext (
@@ -338,6 +351,12 @@ private function getGroupForToken(File $phpcsFile, int $pointer): string
338
351
return self ::SPECIAL_METHODS [$ name ];
339
352
}
340
353
354
+ $ methodGroup = $ this ->resolveMethodGroup ($ phpcsFile , $ pointer , $ name );
355
+
356
+ if ($ methodGroup !== null ) {
357
+ return $ methodGroup ;
358
+ }
359
+
341
360
$ visibility = $ this ->getVisibilityForToken ($ phpcsFile , $ pointer );
342
361
$ isStatic = $ this ->isMemberStatic ($ phpcsFile , $ pointer );
343
362
$ isFinal = $ this ->isMethodFinal ($ phpcsFile , $ pointer );
@@ -387,6 +406,109 @@ private function getGroupForToken(File $phpcsFile, int $pointer): string
387
406
}
388
407
}
389
408
409
+ private function resolveMethodGroup (File $ phpcsFile , int $ pointer , string $ method ): ?string
410
+ {
411
+ foreach ($ this ->getNormalizedMethodGroups () as $ group => $ methodRequirements ) {
412
+ foreach ($ methodRequirements as $ methodRequirement ) {
413
+ if ($ methodRequirement ['name ' ] !== null && $ method !== strtolower ($ methodRequirement ['name ' ])) {
414
+ continue ;
415
+ }
416
+
417
+ if (
418
+ $ this ->hasRequiredAnnotations ($ phpcsFile , $ pointer , $ methodRequirement ['annotations ' ])
419
+ && $ this ->hasRequiredAttributes ($ phpcsFile , $ pointer , $ methodRequirement ['attributes ' ])
420
+ ) {
421
+ return $ group ;
422
+ }
423
+ }
424
+ }
425
+
426
+ return null ;
427
+ }
428
+
429
+ /**
430
+ * @param array<string> $requiredAnnotations
431
+ */
432
+ private function hasRequiredAnnotations (File $ phpcsFile , int $ pointer , array $ requiredAnnotations ): bool
433
+ {
434
+ if ($ requiredAnnotations === []) {
435
+ return true ;
436
+ }
437
+
438
+ $ annotations = [];
439
+
440
+ foreach (AnnotationHelper::getAnnotations ($ phpcsFile , $ pointer ) as $ annotation ) {
441
+ $ annotations [$ annotation ->getName ()] = true ;
442
+ }
443
+
444
+ foreach ($ requiredAnnotations as $ requiredAnnotation ) {
445
+ if (!array_key_exists ('@ ' . $ requiredAnnotation , $ annotations )) {
446
+ return false ;
447
+ }
448
+ }
449
+
450
+ return true ;
451
+ }
452
+
453
+ /**
454
+ * @param array<string> $requiredAttributes
455
+ */
456
+ private function hasRequiredAttributes (File $ phpcsFile , int $ pointer , array $ requiredAttributes ): bool
457
+ {
458
+ if ($ requiredAttributes === []) {
459
+ return true ;
460
+ }
461
+
462
+ $ attributesClassNames = $ this ->getAttributeClassNamesForToken ($ phpcsFile , $ pointer );
463
+
464
+ foreach ($ requiredAttributes as $ requiredAttribute ) {
465
+ if (!array_key_exists (strtolower ($ requiredAttribute ), $ attributesClassNames )) {
466
+ return false ;
467
+ }
468
+ }
469
+
470
+ return true ;
471
+ }
472
+
473
+ /**
474
+ * @return array<string, string>
475
+ */
476
+ private function getAttributeClassNamesForToken (File $ phpcsFile , int $ pointer ): array
477
+ {
478
+ $ tokens = $ phpcsFile ->getTokens ();
479
+ $ attributePointer = null ;
480
+ $ attributes = [];
481
+
482
+ while (true ) {
483
+ $ attributeEndPointerCandidate = TokenHelper::findPrevious (
484
+ $ phpcsFile ,
485
+ [T_ATTRIBUTE_END , T_SEMICOLON , T_CLOSE_CURLY_BRACKET , T_OPEN_CURLY_BRACKET ],
486
+ $ attributePointer ?? $ pointer - 1
487
+ );
488
+
489
+ if (
490
+ $ attributeEndPointerCandidate === null
491
+ || $ tokens [$ attributeEndPointerCandidate ]['code ' ] !== T_ATTRIBUTE_END
492
+ ) {
493
+ break ;
494
+ }
495
+
496
+ $ attributePointer = $ tokens [$ attributeEndPointerCandidate ]['attribute_opener ' ];
497
+
498
+ foreach (AttributeHelper::getAttributes ($ phpcsFile , $ attributePointer ) as $ attribute ) {
499
+ $ attributeClass = NamespaceHelper::resolveClassName (
500
+ $ phpcsFile ,
501
+ $ attribute ->getName (),
502
+ $ attribute ->getStartPointer ()
503
+ );
504
+ $ attributeClass = ltrim ($ attributeClass , '\\' );
505
+ $ attributes [strtolower ($ attributeClass )] = $ attributeClass ;
506
+ }
507
+ }
508
+
509
+ return $ attributes ;
510
+ }
511
+
390
512
private function getVisibilityForToken (File $ phpcsFile , int $ pointer ): int
391
513
{
392
514
$ tokens = $ phpcsFile ->getTokens ();
@@ -547,6 +669,43 @@ private function removeBlankLinesAfterMember(File $phpcsFile, int $memberEndPoin
547
669
return $ linesToRemove ;
548
670
}
549
671
672
+ /**
673
+ * @return array<string, list<array{name: string|null, attributes: array<string>, annotations: array<string>}>>
674
+ */
675
+ private function getNormalizedMethodGroups (): array
676
+ {
677
+ if ($ this ->normalizedMethodGroups === null ) {
678
+ $ this ->normalizedMethodGroups = [];
679
+ $ methodGroups = SniffSettingsHelper::normalizeAssociativeArray ($ this ->methodGroups );
680
+
681
+ foreach ($ methodGroups as $ group => $ groupDefinition ) {
682
+ $ group = strtolower ((string ) $ group );
683
+ $ this ->normalizedMethodGroups [$ group ] = [];
684
+ $ methodDefinitions = preg_split ('~ \\s*, \\s*~ ' , (string ) $ groupDefinition , -1 , PREG_SPLIT_NO_EMPTY );
685
+ /** @var list<string> $methodDefinitions */
686
+ foreach ($ methodDefinitions as $ methodDefinition ) {
687
+ $ tokens = preg_split ('~(?=[#@])~ ' , $ methodDefinition );
688
+ /** @var array<string> $tokens */
689
+ $ method = array_shift ($ tokens );
690
+ $ methodRequirement = [
691
+ 'name ' => $ method !== '' ? $ method : null ,
692
+ 'attributes ' => [],
693
+ 'annotations ' => [],
694
+ ];
695
+
696
+ foreach ($ tokens as $ token ) {
697
+ $ key = $ token [0 ] === '# ' ? 'attributes ' : 'annotations ' ;
698
+ $ methodRequirement [$ key ][] = substr ($ token , 1 );
699
+ }
700
+
701
+ $ this ->normalizedMethodGroups [$ group ][] = $ methodRequirement ;
702
+ }
703
+ }
704
+ }
705
+
706
+ return $ this ->normalizedMethodGroups ;
707
+ }
708
+
550
709
/**
551
710
* @return array<string, int>
552
711
*/
@@ -585,17 +744,19 @@ private function getNormalizedGroups(): array
585
744
self ::GROUP_MAGIC_METHODS ,
586
745
];
587
746
747
+ $ normalizedMethodGroups = $ this ->getNormalizedMethodGroups ();
588
748
$ normalizedGroupsWithShortcuts = [];
589
749
$ order = 1 ;
590
750
foreach (SniffSettingsHelper::normalizeArray ($ this ->groups ) as $ groupsString ) {
591
751
/** @var list<string> $groups */
592
- $ groups = preg_split ('~ \\s*, \\s*~ ' , strtolower ($ groupsString ));
752
+ $ groups = preg_split ('~ \\s*, \\s*~ ' , strtolower ($ groupsString ), - 1 , PREG_SPLIT_NO_EMPTY );
593
753
foreach ($ groups as $ groupOrShortcut ) {
594
754
$ groupOrShortcut = preg_replace ('~ \\s+~ ' , ' ' , $ groupOrShortcut );
595
755
596
756
if (
597
757
!in_array ($ groupOrShortcut , $ supportedGroups , true )
598
758
&& !array_key_exists ($ groupOrShortcut , self ::SHORTCUTS )
759
+ && !array_key_exists ($ groupOrShortcut , $ normalizedMethodGroups )
599
760
) {
600
761
throw new UnsupportedClassGroupException ($ groupOrShortcut );
601
762
}
@@ -608,7 +769,10 @@ private function getNormalizedGroups(): array
608
769
609
770
$ normalizedGroups = [];
610
771
foreach ($ normalizedGroupsWithShortcuts as $ groupOrShortcut => $ groupOrder ) {
611
- if (in_array ($ groupOrShortcut , $ supportedGroups , true )) {
772
+ if (
773
+ in_array ($ groupOrShortcut , $ supportedGroups , true )
774
+ || array_key_exists ($ groupOrShortcut , $ normalizedMethodGroups )
775
+ ) {
612
776
$ normalizedGroups [$ groupOrShortcut ] = $ groupOrder ;
613
777
} else {
614
778
foreach ($ this ->unpackShortcut ($ groupOrShortcut , $ supportedGroups ) as $ group ) {
@@ -624,10 +788,14 @@ private function getNormalizedGroups(): array
624
788
}
625
789
}
626
790
627
- if ($ normalizedGroups === []) {
791
+ if ($ normalizedGroups === [] && $ normalizedMethodGroups === [] ) {
628
792
$ normalizedGroups = array_flip ($ supportedGroups );
629
793
} else {
630
- $ missingGroups = array_diff ($ supportedGroups , array_keys ($ normalizedGroups ));
794
+ $ missingGroups = array_diff (
795
+ array_merge ($ supportedGroups , array_keys ($ normalizedMethodGroups )),
796
+ array_keys ($ normalizedGroups )
797
+ );
798
+
631
799
if ($ missingGroups !== []) {
632
800
throw new MissingClassGroupsException ($ missingGroups );
633
801
}
0 commit comments