@@ -153,7 +153,7 @@ protected function extractExceptions(File $phpcsFile, int $stackPointer): array
153153
154154 // Handle the case where there's no namespace separator, string token, or fully qualified name token
155155 // (e.g., for parenthesis after 'new')
156- if (!$ this ->isGivenKind ([T_NS_SEPARATOR , T_STRING , T_NAME_FULLY_QUALIFIED ], $ tokens [$ contentIndex ])) {
156+ if (!$ this ->isGivenKind ([T_NS_SEPARATOR , T_STRING , T_NAME_FULLY_QUALIFIED , T_NAME_QUALIFIED ], $ tokens [$ contentIndex ])) {
157157 continue ;
158158 }
159159
@@ -248,12 +248,13 @@ protected function extractException(File $phpcsFile, int $contentIndex): array
248248 $ fullClass = $ tokens [$ position ]['content ' ];
249249 ++$ position ;
250250 } else {
251- while ($ this ->isGivenKind ([T_NS_SEPARATOR , T_STRING ], $ tokens [$ position ])) {
251+ while ($ this ->isGivenKind ([T_NS_SEPARATOR , T_STRING , T_NAME_QUALIFIED ], $ tokens [$ position ])) {
252252 $ fullClass .= $ tokens [$ position ]['content ' ];
253253 ++$ position ;
254254 }
255255 }
256256
257+ $ fullyQualified = str_starts_with ($ fullClass , '\\' );
257258 $ class = $ fullClass = ltrim ($ fullClass , '\\' );
258259 $ lastSeparator = strrpos ($ class , '\\' );
259260 if ($ lastSeparator !== false ) {
@@ -265,6 +266,7 @@ protected function extractException(File $phpcsFile, int $contentIndex): array
265266 'end ' => $ position - 1 ,
266267 'fullClass ' => $ fullClass ,
267268 'class ' => $ class ,
269+ 'fullyQualified ' => $ fullyQualified ,
268270 ];
269271 }
270272
@@ -283,6 +285,7 @@ protected function compareExceptionsAndAnnotations(
283285 int $ docBlockEndIndex ,
284286 ): void {
285287 $ useStatements = $ this ->getUseStatements ($ phpcsFile );
288+ $ namespace = $ this ->getNamespaceAt ($ phpcsFile , $ docBlockEndIndex );
286289
287290 foreach ($ annotations as $ annotation ) {
288291 if ($ this ->isInCode ($ annotation , $ exceptions , $ useStatements )) {
@@ -307,7 +310,7 @@ protected function compareExceptionsAndAnnotations(
307310
308311 $ exceptionsToAdd = [];
309312 foreach ($ exceptions as $ exception ) {
310- $ exception = $ this ->normalizeClassName ($ exception , $ useStatements );
313+ $ exception = $ this ->normalizeClassName ($ exception , $ useStatements, $ namespace );
311314 if (empty ($ exception ['fullClass ' ])) {
312315 // We skip for complex scenarios
313316 $ phpcsFile ->addError ('Doc Block @throw annotation missing ' , $ docBlockEndIndex , 'ThrowMissingManual ' );
@@ -333,19 +336,79 @@ protected function compareExceptionsAndAnnotations(
333336 $ this ->addAnnotationLines ($ phpcsFile , $ exceptionsToAdd , $ docBlockEndIndex );
334337 }
335338
339+ /**
340+ * Namespace the given position is declared in; a file can hold several namespace blocks.
341+ *
342+ * @param \PHP_CodeSniffer\Files\File $phpcsFile
343+ * @param int $index
344+ *
345+ * @return string
346+ */
347+ protected function getNamespaceAt (File $ phpcsFile , int $ index ): string
348+ {
349+ $ tokens = $ phpcsFile ->getTokens ();
350+
351+ $ namespaceIndex = $ index ;
352+ do {
353+ $ namespaceIndex = $ phpcsFile ->findPrevious (T_NAMESPACE , $ namespaceIndex - 1 );
354+ if ($ namespaceIndex === false ) {
355+ return '' ;
356+ }
357+ $ nextIndex = $ phpcsFile ->findNext (Tokens::$ emptyTokens , $ namespaceIndex + 1 , null , true );
358+ // `namespace\Foo` is the relative-name operator, not a declaration
359+ } while ($ nextIndex !== false && $ tokens [$ nextIndex ]['code ' ] === T_NS_SEPARATOR );
360+
361+ if (isset ($ tokens [$ namespaceIndex ]['scope_closer ' ]) && $ tokens [$ namespaceIndex ]['scope_closer ' ] < $ index ) {
362+ return '' ;
363+ }
364+
365+ $ endIndex = $ phpcsFile ->findNext ([T_SEMICOLON , T_OPEN_CURLY_BRACKET ], $ namespaceIndex + 1 );
366+ if ($ endIndex === false ) {
367+ return '' ;
368+ }
369+
370+ $ namespace = '' ;
371+ for ($ i = $ namespaceIndex + 1 ; $ i < $ endIndex ; $ i ++) {
372+ if (!in_array ($ tokens [$ i ]['code ' ], Tokens::$ emptyTokens , true )) {
373+ $ namespace .= $ tokens [$ i ]['content ' ];
374+ }
375+ }
376+
377+ return $ namespace ;
378+ }
379+
336380 /**
337381 * @param array<string, mixed> $exception
338382 * @param array<string, array<string, mixed>> $useStatements
383+ * @param string $namespace Namespace of the file, to resolve names that are not imported.
339384 *
340385 * @return array<string, mixed> Exception
341386 */
342- protected function normalizeClassName (array $ exception , array $ useStatements ): array
387+ protected function normalizeClassName (array $ exception , array $ useStatements, string $ namespace = '' ): array
343388 {
389+ if (!empty ($ exception ['fullyQualified ' ]) || empty ($ exception ['fullClass ' ])) {
390+ return $ exception ;
391+ }
392+
393+ /** @var string $fullClass */
394+ $ fullClass = $ exception ['fullClass ' ];
395+ $ segments = explode ('\\' , $ fullClass );
396+ $ first = array_shift ($ segments );
397+
344398 foreach ($ useStatements as $ useStatement ) {
345- if ($ useStatement ['alias ' ] === $ exception ['class ' ] || $ useStatement ['shortName ' ] === $ exception ['class ' ]) {
399+ if ($ useStatement ['alias ' ] !== $ first && $ useStatement ['shortName ' ] !== $ first ) {
400+ continue ;
401+ }
402+ if (!$ segments ) {
346403 $ exception ['class ' ] = $ useStatement ['shortName ' ];
347- $ exception ['fullClass ' ] = $ useStatement ['fullName ' ];
348404 }
405+ $ exception ['fullClass ' ] = implode ('\\' , [$ useStatement ['fullName ' ], ...$ segments ]);
406+
407+ return $ exception ;
408+ }
409+
410+ if ($ namespace !== '' ) {
411+ $ exception ['fullClass ' ] = $ namespace . '\\' . $ fullClass ;
349412 }
350413
351414 return $ exception ;
0 commit comments