diff --git a/PhpCollective/Sniffs/AbstractSniffs/AbstractSniff.php b/PhpCollective/Sniffs/AbstractSniffs/AbstractSniff.php index 9e250cd..c4e5e28 100644 --- a/PhpCollective/Sniffs/AbstractSniffs/AbstractSniff.php +++ b/PhpCollective/Sniffs/AbstractSniffs/AbstractSniff.php @@ -28,6 +28,27 @@ abstract class AbstractSniff implements Sniff '@noinspection', ]; + /** + * Per-file cache for class-name-with-namespace resolution. + * + * `getClassNameWithNamespace()` walks the token stream backwards from + * the end of the file to find the class/trait/interface/enum keyword. + * The result is stable for the lifetime of a phpcs run on a given file, + * but the original implementation re-computed it on every sniff call. + * Caching by filename turns dozens-to-hundreds of full-file walks per + * large file into a single walk. + * + * @var array + */ + private static array $classNameWithNamespaceCache = []; + + /** + * Per-file cache for class name resolution (with filename fallback). + * + * @var array + */ + private static array $classNameCache = []; + /** * @param \PHP_CodeSniffer\Files\File $phpcsFile * @param int $stackPtr @@ -98,22 +119,27 @@ protected function getNamespace(File $phpcsFile): string */ protected function getClassNameWithNamespace(File $phpcsFile): ?string { + $cacheKey = $phpcsFile->getFilename(); + if (array_key_exists($cacheKey, self::$classNameWithNamespaceCache)) { + return self::$classNameWithNamespaceCache[$cacheKey]; + } + try { $lastToken = TokenHelper::getLastTokenPointer($phpcsFile); } catch (EmptyFileException $e) { - return null; + return self::$classNameWithNamespaceCache[$cacheKey] = null; } if (!NamespaceHelper::findCurrentNamespaceName($phpcsFile, $lastToken)) { - return null; + return self::$classNameWithNamespaceCache[$cacheKey] = null; } $prevIndex = $phpcsFile->findPrevious([T_CLASS, T_TRAIT, T_INTERFACE, T_ENUM], $lastToken); if (!$prevIndex) { - return null; + return self::$classNameWithNamespaceCache[$cacheKey] = null; } - return ClassHelper::getFullyQualifiedName( + return self::$classNameWithNamespaceCache[$cacheKey] = ClassHelper::getFullyQualifiedName( $phpcsFile, $prevIndex, ); @@ -126,10 +152,15 @@ protected function getClassNameWithNamespace(File $phpcsFile): ?string */ protected function getClassName(File $phpcsFile): string { + $cacheKey = $phpcsFile->getFilename(); + if (isset(self::$classNameCache[$cacheKey])) { + return self::$classNameCache[$cacheKey]; + } + $namespace = $this->getClassNameWithNamespace($phpcsFile); if ($namespace) { - return trim($namespace, '\\'); + return self::$classNameCache[$cacheKey] = trim($namespace, '\\'); } $fileName = $phpcsFile->getFilename(); @@ -143,7 +174,7 @@ protected function getClassName(File $phpcsFile): string $className = implode('\\', $classNameParts); $className = str_replace('.php', '', $className); - return $className; + return self::$classNameCache[$cacheKey] = $className; } /** diff --git a/PhpCollective/Sniffs/Commenting/DocBlockReturnSelfSniff.php b/PhpCollective/Sniffs/Commenting/DocBlockReturnSelfSniff.php index 8ab8902..8621c1b 100644 --- a/PhpCollective/Sniffs/Commenting/DocBlockReturnSelfSniff.php +++ b/PhpCollective/Sniffs/Commenting/DocBlockReturnSelfSniff.php @@ -43,6 +43,8 @@ public function process(File $phpcsFile, $stackPointer): void $docBlockStartIndex = $tokens[$docBlockEndIndex]['comment_opener']; + $ownClassName = null; + for ($i = $docBlockStartIndex + 1; $i < $docBlockEndIndex; $i++) { if ($tokens[$i]['type'] !== 'T_DOC_COMMENT_TAG') { continue; @@ -75,6 +77,28 @@ public function process(File $phpcsFile, $stackPointer): void } $parts = explode('|', $content); + + // The three assertions below all act only when `parts` contains + // `self`, `$this`, or the own (fully qualified) class name. + // Skipping the expensive return-type body scan when none of these + // are present turns this sniff from O(methods * body_size) into + // ~O(methods) for files where most @return tags are plain types + // like `void`, `int`, `array<...>`, etc. + if ($ownClassName === null) { + $ownClassName = '\\' . $this->getClassName($phpcsFile); + } + $needsReturnTypeCheck = false; + foreach ($parts as $part) { + if ($part === 'self' || $part === '$this' || $part === $ownClassName) { + $needsReturnTypeCheck = true; + + break; + } + } + if (!$needsReturnTypeCheck) { + continue; + } + $returnTypes = $this->getReturnTypes($phpcsFile, $stackPointer); $this->assertCorrectDocBlockParts($phpcsFile, $classNameIndex, $parts, $returnTypes, $appendix);