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 ;
@@ -181,9 +189,15 @@ class ClassStructureSniff implements Sniff
181
189
'__debuginfo ' => self ::GROUP_MAGIC_METHODS ,
182
190
];
183
191
192
+ /** @var array<string, string> */
193
+ public array $ methodGroups = [];
194
+
184
195
/** @var list<string> */
185
196
public array $ groups = [];
186
197
198
+ /** @var array<string, list<array{name: string|null, attributes: array<string>, annotations: array<string>}>>|null */
199
+ private ?array $ normalizedMethodGroups = null ;
200
+
187
201
/** @var array<string, int>|null */
188
202
private ?array $ normalizedGroups = null ;
189
203
@@ -342,6 +356,12 @@ private function getGroupForToken(File $phpcsFile, int $pointer): string
342
356
return self ::SPECIAL_METHODS [$ name ];
343
357
}
344
358
359
+ $ methodGroup = $ this ->resolveMethodGroup ($ phpcsFile , $ pointer , $ name );
360
+
361
+ if ($ methodGroup !== null ) {
362
+ return $ methodGroup ;
363
+ }
364
+
345
365
$ visibility = $ this ->getVisibilityForToken ($ phpcsFile , $ pointer );
346
366
$ isStatic = $ this ->isMemberStatic ($ phpcsFile , $ pointer );
347
367
$ isFinal = $ this ->isMethodFinal ($ phpcsFile , $ pointer );
@@ -391,6 +411,109 @@ private function getGroupForToken(File $phpcsFile, int $pointer): string
391
411
}
392
412
}
393
413
414
+ private function resolveMethodGroup (File $ phpcsFile , int $ pointer , string $ method ): ?string
415
+ {
416
+ foreach ($ this ->getNormalizedMethodGroups () as $ group => $ methodRequirements ) {
417
+ foreach ($ methodRequirements as $ methodRequirement ) {
418
+ if ($ methodRequirement ['name ' ] !== null && $ method !== strtolower ($ methodRequirement ['name ' ])) {
419
+ continue ;
420
+ }
421
+
422
+ if (
423
+ $ this ->hasRequiredAnnotations ($ phpcsFile , $ pointer , $ methodRequirement ['annotations ' ])
424
+ && $ this ->hasRequiredAttributes ($ phpcsFile , $ pointer , $ methodRequirement ['attributes ' ])
425
+ ) {
426
+ return $ group ;
427
+ }
428
+ }
429
+ }
430
+
431
+ return null ;
432
+ }
433
+
434
+ /**
435
+ * @param array<string> $requiredAnnotations
436
+ */
437
+ private function hasRequiredAnnotations (File $ phpcsFile , int $ pointer , array $ requiredAnnotations ): bool
438
+ {
439
+ if ($ requiredAnnotations === []) {
440
+ return true ;
441
+ }
442
+
443
+ $ annotations = [];
444
+
445
+ foreach (AnnotationHelper::getAnnotations ($ phpcsFile , $ pointer ) as $ annotation ) {
446
+ $ annotations [$ annotation ->getName ()] = true ;
447
+ }
448
+
449
+ foreach ($ requiredAnnotations as $ requiredAnnotation ) {
450
+ if (!array_key_exists ('@ ' . $ requiredAnnotation , $ annotations )) {
451
+ return false ;
452
+ }
453
+ }
454
+
455
+ return true ;
456
+ }
457
+
458
+ /**
459
+ * @param array<string> $requiredAttributes
460
+ */
461
+ private function hasRequiredAttributes (File $ phpcsFile , int $ pointer , array $ requiredAttributes ): bool
462
+ {
463
+ if ($ requiredAttributes === []) {
464
+ return true ;
465
+ }
466
+
467
+ $ attributesClassNames = $ this ->getAttributeClassNamesForToken ($ phpcsFile , $ pointer );
468
+
469
+ foreach ($ requiredAttributes as $ requiredAttribute ) {
470
+ if (!array_key_exists (strtolower ($ requiredAttribute ), $ attributesClassNames )) {
471
+ return false ;
472
+ }
473
+ }
474
+
475
+ return true ;
476
+ }
477
+
478
+ /**
479
+ * @return array<string, string>
480
+ */
481
+ private function getAttributeClassNamesForToken (File $ phpcsFile , int $ pointer ): array
482
+ {
483
+ $ tokens = $ phpcsFile ->getTokens ();
484
+ $ attributePointer = null ;
485
+ $ attributes = [];
486
+
487
+ while (true ) {
488
+ $ attributeEndPointerCandidate = TokenHelper::findPrevious (
489
+ $ phpcsFile ,
490
+ [T_ATTRIBUTE_END , T_SEMICOLON , T_CLOSE_CURLY_BRACKET , T_OPEN_CURLY_BRACKET ],
491
+ $ attributePointer ?? $ pointer - 1 ,
492
+ );
493
+
494
+ if (
495
+ $ attributeEndPointerCandidate === null
496
+ || $ tokens [$ attributeEndPointerCandidate ]['code ' ] !== T_ATTRIBUTE_END
497
+ ) {
498
+ break ;
499
+ }
500
+
501
+ $ attributePointer = $ tokens [$ attributeEndPointerCandidate ]['attribute_opener ' ];
502
+
503
+ foreach (AttributeHelper::getAttributes ($ phpcsFile , $ attributePointer ) as $ attribute ) {
504
+ $ attributeClass = NamespaceHelper::resolveClassName (
505
+ $ phpcsFile ,
506
+ $ attribute ->getName (),
507
+ $ attribute ->getStartPointer (),
508
+ );
509
+ $ attributeClass = ltrim ($ attributeClass , '\\' );
510
+ $ attributes [strtolower ($ attributeClass )] = $ attributeClass ;
511
+ }
512
+ }
513
+
514
+ return $ attributes ;
515
+ }
516
+
394
517
private function getVisibilityForToken (File $ phpcsFile , int $ pointer ): int
395
518
{
396
519
$ tokens = $ phpcsFile ->getTokens ();
@@ -551,6 +674,43 @@ private function removeBlankLinesAfterMember(File $phpcsFile, int $memberEndPoin
551
674
return $ linesToRemove ;
552
675
}
553
676
677
+ /**
678
+ * @return array<string, list<array{name: string|null, attributes: array<string>, annotations: array<string>}>>
679
+ */
680
+ private function getNormalizedMethodGroups (): array
681
+ {
682
+ if ($ this ->normalizedMethodGroups === null ) {
683
+ $ this ->normalizedMethodGroups = [];
684
+ $ methodGroups = SniffSettingsHelper::normalizeAssociativeArray ($ this ->methodGroups );
685
+
686
+ foreach ($ methodGroups as $ group => $ groupDefinition ) {
687
+ $ group = strtolower ((string ) $ group );
688
+ $ this ->normalizedMethodGroups [$ group ] = [];
689
+ $ methodDefinitions = preg_split ('~ \\s*, \\s*~ ' , (string ) $ groupDefinition , -1 , PREG_SPLIT_NO_EMPTY );
690
+ /** @var list<string> $methodDefinitions */
691
+ foreach ($ methodDefinitions as $ methodDefinition ) {
692
+ $ tokens = preg_split ('~(?=[#@])~ ' , $ methodDefinition );
693
+ /** @var list<string> $tokens */
694
+ $ method = array_shift ($ tokens );
695
+ $ methodRequirement = [
696
+ 'name ' => $ method !== '' ? $ method : null ,
697
+ 'attributes ' => [],
698
+ 'annotations ' => [],
699
+ ];
700
+
701
+ foreach ($ tokens as $ token ) {
702
+ $ key = $ token [0 ] === '# ' ? 'attributes ' : 'annotations ' ;
703
+ $ methodRequirement [$ key ][] = substr ($ token , 1 );
704
+ }
705
+
706
+ $ this ->normalizedMethodGroups [$ group ][] = $ methodRequirement ;
707
+ }
708
+ }
709
+ }
710
+
711
+ return $ this ->normalizedMethodGroups ;
712
+ }
713
+
554
714
/**
555
715
* @return array<string, int>
556
716
*/
@@ -589,18 +749,20 @@ private function getNormalizedGroups(): array
589
749
self ::GROUP_MAGIC_METHODS ,
590
750
];
591
751
752
+ $ normalizedMethodGroups = $ this ->getNormalizedMethodGroups ();
592
753
$ normalizedGroupsWithShortcuts = [];
593
754
$ order = 1 ;
594
755
foreach (SniffSettingsHelper::normalizeArray ($ this ->groups ) as $ groupsString ) {
595
756
/** @var list<string> $groups */
596
- $ groups = preg_split ('~ \\s*, \\s*~ ' , strtolower ($ groupsString ));
757
+ $ groups = preg_split ('~ \\s*, \\s*~ ' , strtolower ($ groupsString ), - 1 , PREG_SPLIT_NO_EMPTY );
597
758
foreach ($ groups as $ groupOrShortcut ) {
598
759
$ groupOrShortcut = preg_replace ('~ \\s+~ ' , ' ' , $ groupOrShortcut );
599
760
600
761
if (
601
762
!in_array ($ groupOrShortcut , $ supportedGroups , true )
602
763
&& !array_key_exists ($ groupOrShortcut , self ::SHORTCUTS )
603
764
&& $ groupOrShortcut !== self ::GROUP_INVOKE_METHOD
765
+ && !array_key_exists ($ groupOrShortcut , $ normalizedMethodGroups )
604
766
) {
605
767
throw new UnsupportedClassGroupException ($ groupOrShortcut );
606
768
}
@@ -613,7 +775,11 @@ private function getNormalizedGroups(): array
613
775
614
776
$ normalizedGroups = [];
615
777
foreach ($ normalizedGroupsWithShortcuts as $ groupOrShortcut => $ groupOrder ) {
616
- if (in_array ($ groupOrShortcut , $ supportedGroups , true ) || $ groupOrShortcut === self ::GROUP_INVOKE_METHOD ) {
778
+ if (
779
+ in_array ($ groupOrShortcut , $ supportedGroups , true )
780
+ || $ groupOrShortcut === self ::GROUP_INVOKE_METHOD
781
+ || array_key_exists ($ groupOrShortcut , $ normalizedMethodGroups )
782
+ ) {
617
783
$ normalizedGroups [$ groupOrShortcut ] = $ groupOrder ;
618
784
} else {
619
785
foreach ($ this ->unpackShortcut ($ groupOrShortcut , $ supportedGroups ) as $ group ) {
@@ -629,12 +795,16 @@ private function getNormalizedGroups(): array
629
795
}
630
796
}
631
797
632
- if ($ normalizedGroups === []) {
798
+ if ($ normalizedGroups === [] && $ normalizedMethodGroups === [] ) {
633
799
$ normalizedGroups = array_flip ($ supportedGroups );
634
800
} else {
635
- $ missingGroups = array_values (array_diff ($ supportedGroups , array_keys ($ normalizedGroups )));
801
+ $ missingGroups = array_diff (
802
+ array_merge ($ supportedGroups , array_keys ($ normalizedMethodGroups )),
803
+ array_keys ($ normalizedGroups ),
804
+ );
805
+
636
806
if ($ missingGroups !== []) {
637
- throw new MissingClassGroupsException ($ missingGroups );
807
+ throw new MissingClassGroupsException (array_values ( $ missingGroups) );
638
808
}
639
809
}
640
810
0 commit comments