22
33namespace SlevomatCodingStandard \Sniffs \TypeHints ;
44
5+ use SlevomatCodingStandard \Helpers \Annotation ;
56use SlevomatCodingStandard \Helpers \AnnotationHelper ;
67use SlevomatCodingStandard \Helpers \DocCommentHelper ;
78use SlevomatCodingStandard \Helpers \FunctionHelper ;
89use SlevomatCodingStandard \Helpers \NamespaceHelper ;
910use SlevomatCodingStandard \Helpers \PropertyHelper ;
11+ use SlevomatCodingStandard \Helpers \ReturnTypeHint ;
1012use SlevomatCodingStandard \Helpers \SniffSettingsHelper ;
1113use SlevomatCodingStandard \Helpers \SuppressHelper ;
1214use SlevomatCodingStandard \Helpers \TokenHelper ;
@@ -29,6 +31,10 @@ class TypeHintDeclarationSniff implements \PHP_CodeSniffer_Sniff
2931
3032 const CODE_MISSING_TRAVERSABLE_RETURN_TYPE_HINT_SPECIFICATION = 'MissingTraversableReturnTypeHintSpecification ' ;
3133
34+ const CODE_USELESS_PARAMETER_ANNOTATION = 'UselessParameterAnnotation ' ;
35+
36+ const CODE_USELESS_RETURN_ANNOTATION = 'UselessReturnAnnotation ' ;
37+
3238 const CODE_USELESS_DOC_COMMENT = 'UselessDocComment ' ;
3339
3440 /** @var bool */
@@ -37,6 +43,9 @@ class TypeHintDeclarationSniff implements \PHP_CodeSniffer_Sniff
3743 /** @var bool */
3844 public $ enableVoidTypeHint = PHP_VERSION_ID >= 70100 ;
3945
46+ /** @var bool */
47+ public $ enableEachParameterAndReturnInspection = false ;
48+
4049 /** @var string[] */
4150 public $ traversableTypeHints = [];
4251
@@ -451,112 +460,155 @@ private function checkReturnTypeHints(\PHP_CodeSniffer_File $phpcsFile, int $fun
451460
452461 private function checkUselessDocComment (\PHP_CodeSniffer_File $ phpcsFile , int $ functionPointer )
453462 {
454- if (SuppressHelper::isSniffSuppressed ($ phpcsFile , $ functionPointer , $ this ->getSniffName (self ::CODE_USELESS_DOC_COMMENT ))) {
463+ $ docCommentSniffSuppressed = SuppressHelper::isSniffSuppressed ($ phpcsFile , $ functionPointer , $ this ->getSniffName (self ::CODE_USELESS_DOC_COMMENT ));
464+ $ returnSniffSuppressed = SuppressHelper::isSniffSuppressed ($ phpcsFile , $ functionPointer , $ this ->getSniffName (self ::CODE_USELESS_RETURN_ANNOTATION ));
465+ $ parameterSniffSuppressed = SuppressHelper::isSniffSuppressed ($ phpcsFile , $ functionPointer , $ this ->getSniffName (self ::CODE_USELESS_PARAMETER_ANNOTATION ));
466+
467+ if ($ docCommentSniffSuppressed && $ returnSniffSuppressed && $ parameterSniffSuppressed ) {
455468 return ;
456469 }
457470
458471 if (!DocCommentHelper::hasDocComment ($ phpcsFile , $ functionPointer )) {
459472 return ;
460473 }
461474
462- if (DocCommentHelper::hasDocCommentDescription ($ phpcsFile , $ functionPointer )) {
463- return ;
464- }
475+ $ containsUsefulInformation = DocCommentHelper::hasDocCommentDescription ($ phpcsFile , $ functionPointer );
465476
466477 foreach (FunctionHelper::getParametersAnnotations ($ phpcsFile , $ functionPointer ) as $ parameterAnnotation ) {
467478 if ($ parameterAnnotation ->getContent () !== null && preg_match ('~^ \\S+ \\s+(?:(?:\.{3} \\s*)?\$ \\S+ \\s+)?[^$]~ ' , $ parameterAnnotation ->getContent ())) {
468- return ;
479+ $ containsUsefulInformation = true ;
480+ break ;
469481 }
470482 }
471483
472- $ isAbstract = FunctionHelper::isAbstract ($ phpcsFile , $ functionPointer );
484+ $ returnTypeHint = FunctionHelper::findReturnTypeHint ($ phpcsFile , $ functionPointer );
485+ $ returnAnnotation = FunctionHelper::findReturnAnnotation ($ phpcsFile , $ functionPointer );
486+ $ isReturnAnnotationUseless = $ this ->isReturnAnnotationUseless ($ phpcsFile , $ functionPointer , $ returnTypeHint , $ returnAnnotation );
473487
474- $ typeHintEqualsAnnotation = function ( string $ typeHint , string $ typeHintInAnnotation ) use ( $ phpcsFile , $ functionPointer ): bool {
475- return TypeHintHelper:: isSimpleTypeHint ( $ typeHint ) || TypeHintHelper:: getFullyQualifiedTypeHint ( $ phpcsFile , $ functionPointer , $ typeHint ) === TypeHintHelper:: getFullyQualifiedTypeHint ($ phpcsFile , $ functionPointer, $ typeHintInAnnotation );
476- } ;
488+ $ parameterTypeHints = FunctionHelper:: getParametersTypeHints ( $ phpcsFile , $ functionPointer );
489+ $ parametersAnnotationTypeHints = $ this -> getFunctionParameterTypeHintsDefinitions ($ phpcsFile , $ functionPointer );
490+ $ uselessParameterNames = $ this -> getUselessParameterNames ( $ phpcsFile , $ functionPointer , $ parameterTypeHints , $ parametersAnnotationTypeHints ) ;
477491
478- if ($ isAbstract || FunctionHelper::returnsValue ($ phpcsFile , $ functionPointer )) {
479- $ returnTypeHint = FunctionHelper::findReturnTypeHint ($ phpcsFile , $ functionPointer );
480- $ returnAnnotation = FunctionHelper::findReturnAnnotation ($ phpcsFile , $ functionPointer );
492+ foreach (AnnotationHelper::getAnnotations ($ phpcsFile , $ functionPointer ) as list ($ annotation )) {
493+ if ($ annotation ->getName () === SuppressHelper::ANNOTATION ) {
494+ $ containsUsefulInformation = true ;
495+ break ;
496+ }
481497
482- if ($ returnAnnotation !== null ) {
483- if ($ returnTypeHint === null ) {
484- return ;
498+ foreach ($ this ->getNormalizedUsefulAnnotations () as $ usefulAnnotation ) {
499+ if ($ annotation ->getName () === $ usefulAnnotation ) {
500+ $ containsUsefulInformation = true ;
501+ break ;
485502 }
486503
487- if ($ this ->isTraversableTypeHint (TypeHintHelper::getFullyQualifiedTypeHint ($ phpcsFile , $ functionPointer , $ returnTypeHint ->getTypeHint ()))) {
488- return ;
504+ if (substr ($ usefulAnnotation , -1 ) === '\\' && strpos ($ annotation ->getName (), $ usefulAnnotation ) === 0 ) {
505+ $ containsUsefulInformation = true ;
506+ break ;
489507 }
508+ }
509+ }
490510
491- if ($ returnAnnotation !== null ) {
492- if (preg_match ('~^ \\S+ \\s+ \\S+~ ' , $ returnAnnotation ->getContent ())) {
493- return ;
494- }
511+ $ isWholeDocCommentUseless = !$ containsUsefulInformation
512+ && ($ returnAnnotation === null || $ isReturnAnnotationUseless )
513+ && count ($ uselessParameterNames ) === count ($ parametersAnnotationTypeHints );
495514
496- $ returnTypeHintsDefinition = preg_split ('~ \\s+~ ' , $ returnAnnotation ->getContent ())[0 ];
497- if ($ this ->definitionContainsStaticOrThisTypeHint ($ returnTypeHintsDefinition )) {
498- return ;
499- } elseif ($ this ->enableNullableTypeHints && $ this ->definitionContainsJustTwoTypeHints ($ returnTypeHintsDefinition ) && $ this ->definitionContainsNullTypeHint ($ returnTypeHintsDefinition )) {
500- $ returnTypeHintDefinitionParts = explode ('| ' , $ returnTypeHintsDefinition );
501- $ returnTypeHintInAnnotation = strtolower ($ returnTypeHintDefinitionParts [0 ]) === 'null ' ? $ returnTypeHintDefinitionParts [1 ] : $ returnTypeHintDefinitionParts [0 ];
502- if (!$ typeHintEqualsAnnotation ($ returnTypeHint ->getTypeHint (), $ returnTypeHintInAnnotation )) {
503- return ;
515+ if ($ this ->enableEachParameterAndReturnInspection && (!$ isWholeDocCommentUseless || $ docCommentSniffSuppressed )) {
516+ if ($ returnAnnotation !== null && $ isReturnAnnotationUseless && !$ returnSniffSuppressed ) {
517+ $ fix = $ phpcsFile ->addFixableError (
518+ sprintf (
519+ '%s %s() has useless @return annotation. ' ,
520+ $ this ->getFunctionTypeLabel ($ phpcsFile , $ functionPointer ),
521+ FunctionHelper::getFullyQualifiedName ($ phpcsFile , $ functionPointer )
522+ ),
523+ $ functionPointer ,
524+ self ::CODE_USELESS_RETURN_ANNOTATION
525+ );
526+ if ($ fix ) {
527+ $ tokens = $ phpcsFile ->getTokens ();
528+ $ docCommentOpenPointer = DocCommentHelper::findDocCommentOpenToken ($ phpcsFile , $ functionPointer );
529+ $ docCommentClosePointer = $ tokens [$ docCommentOpenPointer ]['comment_closer ' ];
530+
531+ for ($ i = $ docCommentOpenPointer + 1 ; $ i < $ docCommentClosePointer ; $ i ++) {
532+ if ($ tokens [$ i ]['code ' ] !== T_DOC_COMMENT_TAG ) {
533+ continue ;
504534 }
505- } elseif (!$ this ->definitionContainsOneTypeHint ($ returnTypeHintsDefinition )) {
506- return ;
507- } elseif (!$ typeHintEqualsAnnotation ($ returnTypeHint ->getTypeHint (), $ returnTypeHintsDefinition )) {
508- return ;
509- }
510- }
511- }
512- }
513535
514- $ parametersTypeHintsDefinitions = $ this ->getFunctionParameterTypeHintsDefinitions ($ phpcsFile , $ functionPointer );
515- foreach (FunctionHelper::getParametersTypeHints ($ phpcsFile , $ functionPointer ) as $ parameterName => $ parameterTypeHint ) {
516- if ($ parameterTypeHint === null ) {
517- if (array_key_exists ($ parameterName , $ parametersTypeHintsDefinitions )) {
518- return ;
519- } else {
520- continue ;
521- }
522- }
536+ if ($ tokens [$ i ]['content ' ] !== '@return ' ) {
537+ continue ;
538+ }
523539
524- if ($ this ->isTraversableTypeHint (TypeHintHelper::getFullyQualifiedTypeHint ($ phpcsFile , $ functionPointer , $ parameterTypeHint ->getTypeHint ()))) {
525- return ;
526- }
540+ $ changeStart = $ phpcsFile ->findPrevious ([T_DOC_COMMENT_STAR ], $ i - 1 , $ docCommentOpenPointer );
541+ $ changeEnd = $ phpcsFile ->findNext ([T_DOC_COMMENT_CLOSE_TAG , T_DOC_COMMENT_STAR ], $ i - 1 , $ docCommentClosePointer + 1 ) - 1 ;
542+ $ phpcsFile ->fixer ->beginChangeset ();
543+ for ($ j = $ changeStart ; $ j <= $ changeEnd ; $ j ++) {
544+ $ phpcsFile ->fixer ->replaceToken ($ j , '' );
545+ }
546+ $ phpcsFile ->fixer ->endChangeset ();
527547
528- if (array_key_exists ($ parameterName , $ parametersTypeHintsDefinitions )) {
529- $ parameterTypeHintDefinition = $ parametersTypeHintsDefinitions [$ parameterName ];
530- if ($ this ->definitionContainsStaticOrThisTypeHint ($ parameterTypeHintDefinition )) {
531- return ;
532- } elseif ($ this ->definitionContainsJustTwoTypeHints ($ parameterTypeHintDefinition ) && $ this ->definitionContainsNullTypeHint ($ parameterTypeHintDefinition )) {
533- $ parameterTypeHintDefinitionParts = explode ('| ' , $ parameterTypeHintDefinition );
534- $ parameterTypeHintInAnnotation = strtolower ($ parameterTypeHintDefinitionParts [0 ]) === 'null ' ? $ parameterTypeHintDefinitionParts [1 ] : $ parameterTypeHintDefinitionParts [0 ];
535- if (!$ typeHintEqualsAnnotation ($ parameterTypeHint ->getTypeHint (), $ parameterTypeHintInAnnotation )) {
536- return ;
548+ break ;
537549 }
538- } elseif (!$ this ->definitionContainsOneTypeHint ($ parameterTypeHintDefinition )) {
539- return ;
540- } elseif (!$ typeHintEqualsAnnotation ($ parameterTypeHint ->getTypeHint (), $ parameterTypeHintDefinition )) {
541- return ;
542550 }
543551 }
544- }
545552
546- foreach (AnnotationHelper::getAnnotations ($ phpcsFile , $ functionPointer ) as list ($ annotation )) {
547- if ($ annotation ->getName () === SuppressHelper::ANNOTATION ) {
548- return ;
553+ if (!$ parameterSniffSuppressed ) {
554+ foreach ($ uselessParameterNames as $ uselessParameterName ) {
555+ $ fix = $ phpcsFile ->addFixableError (
556+ sprintf (
557+ '%s %s() has useless @param annotation for parameter %s. ' ,
558+ $ this ->getFunctionTypeLabel ($ phpcsFile , $ functionPointer ),
559+ FunctionHelper::getFullyQualifiedName ($ phpcsFile , $ functionPointer ),
560+ $ uselessParameterName
561+ ),
562+ $ functionPointer ,
563+ self ::CODE_USELESS_PARAMETER_ANNOTATION
564+ );
565+ if ($ fix ) {
566+ $ tokens = $ phpcsFile ->getTokens ();
567+ $ docCommentOpenPointer = DocCommentHelper::findDocCommentOpenToken ($ phpcsFile , $ functionPointer );
568+ $ docCommentClosePointer = $ tokens [$ docCommentOpenPointer ]['comment_closer ' ];
569+
570+ for ($ i = $ docCommentOpenPointer + 1 ; $ i < $ docCommentClosePointer ; $ i ++) {
571+ if ($ tokens [$ i ]['code ' ] !== T_DOC_COMMENT_TAG ) {
572+ continue ;
573+ }
574+
575+ if ($ tokens [$ i ]['content ' ] !== '@param ' ) {
576+ continue ;
577+ }
578+
579+ $ parameterInformationPointer = $ phpcsFile ->findNext ([T_DOC_COMMENT_WHITESPACE ], $ i + 1 , $ docCommentClosePointer + 1 , true );
580+
581+ if ($ parameterInformationPointer === false || $ tokens [$ parameterInformationPointer ]['code ' ] !== T_DOC_COMMENT_STRING ) {
582+ continue ;
583+ }
584+
585+ if (!preg_match ('~\S+\s+(\$\S+)~ ' , $ tokens [$ parameterInformationPointer ]['content ' ], $ match )) {
586+ continue ;
587+ }
588+
589+ if (!in_array ($ match [1 ], $ uselessParameterNames , true )) {
590+ continue ;
591+ }
592+
593+ $ changeStart = $ phpcsFile ->findPrevious ([T_DOC_COMMENT_STAR ], $ i - 1 );
594+ $ changeEnd = $ phpcsFile ->findNext ([T_DOC_COMMENT_CLOSE_TAG , T_DOC_COMMENT_STAR ], $ i - 1 ) - 1 ;
595+ $ phpcsFile ->fixer ->beginChangeset ();
596+ for ($ j = $ changeStart ; $ j <= $ changeEnd ; $ j ++) {
597+ $ phpcsFile ->fixer ->replaceToken ($ j , '' );
598+ }
599+ $ phpcsFile ->fixer ->endChangeset ();
600+
601+ break ;
602+ }
603+ }
604+ }
549605 }
550606
551- foreach ($ this ->getNormalizedUsefulAnnotations () as $ usefulAnnotation ) {
552- if ($ annotation ->getName () === $ usefulAnnotation ) {
553- return ;
554- }
607+ return ;
608+ }
555609
556- if (substr ($ usefulAnnotation , -1 ) === '\\' && strpos ($ annotation ->getName (), $ usefulAnnotation ) === 0 ) {
557- return ;
558- }
559- }
610+ if (!$ isWholeDocCommentUseless || $ docCommentSniffSuppressed ) {
611+ return ;
560612 }
561613
562614 $ fix = $ phpcsFile ->addFixableError (
@@ -741,4 +793,103 @@ private function getFunctionParameterTypeHintsDefinitions(\PHP_CodeSniffer_File
741793 return $ parametersTypeHintsDefinitions ;
742794 }
743795
796+ private function typeHintEqualsAnnotation (\PHP_CodeSniffer_File $ phpcsFile , int $ functionPointer , string $ typeHint , string $ typeHintInAnnotation ): bool
797+ {
798+ return TypeHintHelper::isSimpleTypeHint ($ typeHint )
799+ || TypeHintHelper::getFullyQualifiedTypeHint ($ phpcsFile , $ functionPointer , $ typeHint ) === TypeHintHelper::getFullyQualifiedTypeHint ($ phpcsFile , $ functionPointer , $ typeHintInAnnotation );
800+ }
801+
802+ private function isReturnAnnotationUseless (\PHP_CodeSniffer_File $ phpcsFile , int $ functionPointer , ReturnTypeHint $ returnTypeHint = null , Annotation $ returnAnnotation = null ): bool
803+ {
804+ if (!FunctionHelper::isAbstract ($ phpcsFile , $ functionPointer ) && !FunctionHelper::returnsValue ($ phpcsFile , $ functionPointer ) && $ returnTypeHint === null ) {
805+ return true ;
806+ }
807+
808+ if ($ returnTypeHint === null || $ returnAnnotation === null || $ returnAnnotation ->getContent () === null ) {
809+ return false ;
810+ }
811+
812+ if (preg_match ('~^ \\S+ \\s+ \\S+~ ' , $ returnAnnotation ->getContent ())) {
813+ return false ;
814+ }
815+
816+ $ returnTypeHintDefinition = preg_split ('~ \\s+~ ' , $ returnAnnotation ->getContent ())[0 ];
817+
818+ if ($ this ->isTraversableTypeHint (TypeHintHelper::getFullyQualifiedTypeHint ($ phpcsFile , $ functionPointer , $ returnTypeHint ->getTypeHint ()))) {
819+ return false ;
820+ }
821+
822+ if ($ this ->definitionContainsStaticOrThisTypeHint ($ returnTypeHintDefinition )) {
823+ return false ;
824+ }
825+
826+ if ($ this ->enableNullableTypeHints && $ this ->isTypeHintDefinitionCompoundOfNull ($ returnTypeHintDefinition )) {
827+ return $ this ->typeHintEqualsAnnotation ($ phpcsFile , $ functionPointer , $ returnTypeHint ->getTypeHint (), $ this ->getTypeFromNullableTypeHintDefinition ($ returnTypeHintDefinition ));
828+ }
829+
830+ if (!$ this ->definitionContainsOneTypeHint ($ returnTypeHintDefinition )) {
831+ return false ;
832+ }
833+
834+ if (!$ this ->typeHintEqualsAnnotation ($ phpcsFile , $ functionPointer , $ returnTypeHint ->getTypeHint (), $ returnTypeHintDefinition )) {
835+ return false ;
836+ }
837+
838+ return true ;
839+ }
840+
841+ private function isTypeHintDefinitionCompoundOfNull (string $ definition ): bool
842+ {
843+ return $ this ->definitionContainsJustTwoTypeHints ($ definition ) && $ this ->definitionContainsNullTypeHint ($ definition );
844+ }
845+
846+ private function getTypeFromNullableTypeHintDefinition (string $ definition ): string
847+ {
848+ $ defitionParts = explode ('| ' , $ definition );
849+ return strtolower ($ defitionParts [0 ]) === 'null ' ? $ defitionParts [1 ] : $ defitionParts [0 ];
850+ }
851+
852+ /**
853+ * @param \PHP_CodeSniffer_File $phpcsFile
854+ * @param int $functionPointer
855+ * @param \SlevomatCodingStandard\Helpers\ParameterTypeHint[]|null[] $functionTypeHints
856+ * @param string[]|null[] $parametersTypeHintsDefinitions
857+ * @return string[] names of parameters with useless annotation hint
858+ */
859+ private function getUselessParameterNames (\PHP_CodeSniffer_File $ phpcsFile , int $ functionPointer , array $ functionTypeHints , array $ parametersTypeHintsDefinitions ): array
860+ {
861+ $ uselessParameterNames = [];
862+
863+ foreach ($ functionTypeHints as $ parameterName => $ parameterTypeHint ) {
864+ if ($ parameterTypeHint === null ) {
865+ continue ;
866+ }
867+
868+ if (!array_key_exists ($ parameterName , $ parametersTypeHintsDefinitions )) {
869+ continue ;
870+ }
871+
872+ if ($ this ->isTraversableTypeHint (TypeHintHelper::getFullyQualifiedTypeHint ($ phpcsFile , $ functionPointer , $ parameterTypeHint ->getTypeHint ()))) {
873+ continue ;
874+ }
875+
876+ $ parameterTypeHintDefinition = $ parametersTypeHintsDefinitions [$ parameterName ];
877+ if ($ this ->definitionContainsStaticOrThisTypeHint ($ parameterTypeHintDefinition )) {
878+ continue ;
879+ } elseif ($ this ->isTypeHintDefinitionCompoundOfNull ($ parameterTypeHintDefinition )) {
880+ if (!$ this ->typeHintEqualsAnnotation ($ phpcsFile , $ functionPointer , $ parameterTypeHint ->getTypeHint (), $ this ->getTypeFromNullableTypeHintDefinition ($ parameterTypeHintDefinition ))) {
881+ continue ;
882+ }
883+ } elseif (!$ this ->definitionContainsOneTypeHint ($ parameterTypeHintDefinition )) {
884+ continue ;
885+ } elseif (!$ this ->typeHintEqualsAnnotation ($ phpcsFile , $ functionPointer , $ parameterTypeHint ->getTypeHint (), $ parameterTypeHintDefinition )) {
886+ continue ;
887+ }
888+
889+ $ uselessParameterNames [] = $ parameterName ;
890+ }
891+
892+ return $ uselessParameterNames ;
893+ }
894+
744895}
0 commit comments