Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Internal/Generics/TemplateManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ private static function computeClassInheritedBindings(string $actualClassName):
/** @var array<string, TypeNode> $bindings */
$bindings = [];

if (! class_exists($actualClassName) && ! interface_exists($actualClassName) && ! trait_exists($actualClassName)) {
if (! class_exists($actualClassName, false) && ! class_exists($actualClassName) && ! interface_exists($actualClassName) && ! trait_exists($actualClassName)) {
return [];
}

Expand Down
21 changes: 20 additions & 1 deletion src/Internal/Io/StreamWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@ private static function extractAndSeedFileMetadata(array $stmts, string $filePat
$namespace = '';
$imports = [];
$classTraitUseDocs = [];
$anonymousTraitUseDocs = [];

$nodesToScan = $stmts;
foreach ($stmts as $stmt) {
Expand Down Expand Up @@ -872,6 +873,24 @@ private static function extractAndSeedFileMetadata(array $stmts, string $filePat
}
}

SpecialTypeResolver::seedFileMetadata($filePath, $namespace, $imports, $classTraitUseDocs);
$nodeFinder = new \PhpParser\NodeFinder();
/** @var list<\PhpParser\Node\Stmt\Class_> $anonClasses */
$anonClasses = $nodeFinder->find($stmts, function (\PhpParser\Node $node): bool {
return $node instanceof \PhpParser\Node\Stmt\Class_ && $node->name === null;
});

foreach ($anonClasses as $anonClass) {
$startLine = $anonClass->getStartLine();
foreach ($anonClass->stmts as $classStmt) {
if ($classStmt instanceof \PhpParser\Node\Stmt\TraitUse) {
$doc = $classStmt->getDocComment();
if ($doc !== null) {
$anonymousTraitUseDocs[$startLine][] = $doc->getText();
}
}
}
}

SpecialTypeResolver::seedFileMetadata($filePath, $namespace, $imports, $classTraitUseDocs, $anonymousTraitUseDocs);
}
}
3 changes: 2 additions & 1 deletion src/Internal/Resolver/HierarchyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ private static function typeExists(string $className): bool
return self::$classExistsCache[$className];
}

$exists = class_exists($className)
$exists = class_exists($className, false)
|| class_exists($className)
|| interface_exists($className)
|| trait_exists($className)
|| enum_exists($className);
Expand Down
215 changes: 184 additions & 31 deletions src/Internal/Resolver/SpecialTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,22 @@ final class SpecialTypeResolver
*/
private static array $classTraitUseDocs = [];

/**
* In-memory cache of anonymous class trait use statement docblocks keyed by file and start line.
*
* @var array<string, array<int, array<int, string>>>
*/
private static array $anonymousTraitUseDocs = [];

/**
* Resets reflection context and dynamic caches. Preserves static file imports.
*/
public static function reset(): void
{
self::$reflectionContextCache = [];
self::$fqcnCache = [];
self::$classTraitUseDocs = [];
self::$anonymousTraitUseDocs = [];
}

/**
Expand Down Expand Up @@ -198,7 +207,7 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct

if ($node instanceof GenericTypeNode) {
$genericType = self::resolve($node->type, $context, $thisObj);
$innerTypes = array_map(fn ($t) => self::resolve($t, $context, $thisObj), $node->genericTypes);
$innerTypes = array_map(fn($t) => self::resolve($t, $context, $thisObj), $node->genericTypes);

return new GenericTypeNode(
$genericType instanceof IdentifierTypeNode ? $genericType : $node->type,
Expand Down Expand Up @@ -252,11 +261,11 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct
}

if ($node instanceof UnionTypeNode) {
return new UnionTypeNode(array_map(fn ($t) => self::resolve($t, $context, $thisObj), $node->types));
return new UnionTypeNode(array_map(fn($t) => self::resolve($t, $context, $thisObj), $node->types));
}

if ($node instanceof IntersectionTypeNode) {
return new IntersectionTypeNode(array_map(fn ($t) => self::resolve($t, $context, $thisObj), $node->types));
return new IntersectionTypeNode(array_map(fn($t) => self::resolve($t, $context, $thisObj), $node->types));
}

return $node;
Expand Down Expand Up @@ -290,7 +299,7 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode

if ($node instanceof GenericTypeNode) {
$genericType = self::resolveForFile($node->type, $file);
$innerTypes = array_map(fn ($t) => self::resolveForFile($t, $file), $node->genericTypes);
$innerTypes = array_map(fn($t) => self::resolveForFile($t, $file), $node->genericTypes);

return new GenericTypeNode(
$genericType instanceof IdentifierTypeNode ? $genericType : $node->type,
Expand Down Expand Up @@ -344,11 +353,11 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode
}

if ($node instanceof UnionTypeNode) {
return new UnionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types));
return new UnionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types));
}

if ($node instanceof IntersectionTypeNode) {
return new IntersectionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types));
return new IntersectionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types));
}

return clone $node;
Expand Down Expand Up @@ -817,16 +826,25 @@ private static function resolveConstantKeyValue(string $fqcn, string $constName)
*
* @param array<string, string> $imports
* @param array<string, array<int, string>> $classTraitUseDocs
* @param array<int, array<int, string>> $anonymousTraitUseDocs
*/
public static function seedFileMetadata(string $fileName, string $namespace, array $imports, array $classTraitUseDocs = []): void
{
public static function seedFileMetadata(
string $fileName,
string $namespace,
array $imports,
array $classTraitUseDocs = [],
array $anonymousTraitUseDocs = []
): void {
if ($fileName !== '') {
$fileName = str_replace('\\', '/', $fileName);
self::$fileNamespaces[$fileName] = $namespace;
self::$fileUseImports[$fileName] = $imports;
foreach ($classTraitUseDocs as $className => $docs) {
self::$classTraitUseDocs[$className] = $docs;
}
if ($anonymousTraitUseDocs !== []) {
self::$anonymousTraitUseDocs[$fileName] = $anonymousTraitUseDocs;
}
}
}

Expand All @@ -841,17 +859,65 @@ public static function getClassTraitUseDocs(string $className): array
return self::$classTraitUseDocs[$className];
}

if (! class_exists($className) && ! trait_exists($className)) {
if (! class_exists($className, false) && ! class_exists($className) && ! trait_exists($className)) {
return self::$classTraitUseDocs[$className] = [];
}

try {
$ref = new \ReflectionClass($className);
$fileName = $ref->getFileName();
if ($fileName !== false && file_exists($fileName)) {
$source = file_get_contents($fileName);
if ($source !== false) {
self::parseFileMetadata($fileName, $source);
if ($fileName !== false) {
$normalizedFile = str_replace('\\', '/', $fileName);

if ($ref->isAnonymous()) {
$startLine = $ref->getStartLine();
$endLine = $ref->getEndLine();

if ($startLine === false || $endLine === false) {
return self::$classTraitUseDocs[$className] = [];
}

if (! isset(self::$anonymousTraitUseDocs[$normalizedFile]) && file_exists($fileName)) {
$source = file_get_contents($fileName);
if ($source !== false) {
self::parseFileMetadata($fileName, $source);
}
}

if (isset(self::$anonymousTraitUseDocs[$normalizedFile][$startLine])) {
return self::$classTraitUseDocs[$className] = self::$anonymousTraitUseDocs[$normalizedFile][$startLine];
}

if (isset(self::$anonymousTraitUseDocs[$normalizedFile])) {
foreach (self::$anonymousTraitUseDocs[$normalizedFile] as $line => $docs) {
if ($line >= $startLine && $line <= $endLine) {
return self::$classTraitUseDocs[$className] = $docs;
}
}

$bestMatch = null;
$minDiff = PHP_INT_MAX;
foreach (self::$anonymousTraitUseDocs[$normalizedFile] as $line => $docs) {
$diff = abs($line - $startLine);
if ($diff <= 5 && $diff < $minDiff) {
$minDiff = $diff;
$bestMatch = $docs;
}
}

if ($bestMatch !== null) {
return self::$classTraitUseDocs[$className] = $bestMatch;
}
}

return self::$classTraitUseDocs[$className] = [];
}

if (file_exists($fileName)) {
$source = file_get_contents($fileName);
if ($source !== false) {
self::parseFileMetadata($fileName, $source);
}
}
}
} catch (\Throwable $e) {
Expand Down Expand Up @@ -1079,15 +1145,22 @@ public static function isBuiltInTypeKeyword(string $name): bool
*/
public static function parseFileMetadata(string $fileName, string $source): void
{
self::$fileNamespaces[$fileName] = '';
self::$fileUseImports[$fileName] = [];
$normalizedFile = str_replace('\\', '/', $fileName);
self::$fileNamespaces[$normalizedFile] = '';
self::$fileUseImports[$normalizedFile] = [];
self::$anonymousTraitUseDocs[$normalizedFile] = [];

try {
$tokens = \PhpToken::tokenize($source);
$count = \count($tokens);
$namespace = '';
$imports = [];
$currentClass = null;

/** @var list<array{type: 'named'|'anon', name?: string, line?: int, depth: int}> $classStack */
$classStack = [];
/** @var array{type: 'named'|'anon', name?: string, line?: int}|null $pendingClass */
$pendingClass = null;
$braceDepth = 0;

for ($i = 0; $i < $count; $i++) {
$token = $tokens[$i];
Expand All @@ -1105,35 +1178,115 @@ public static function parseFileMetadata(string $fileName, string $source): void
}
}
$namespace = trim(implode('', $nsParts));
self::$fileNamespaces[$fileName] = $namespace;
self::$fileNamespaces[$normalizedFile] = $namespace;

continue;
}

if (($token->id === T_CLASS || $token->id === T_INTERFACE || $token->id === T_TRAIT || (\defined('T_ENUM') && $token->id === T_ENUM)) && isset($tokens[$i + 2]) && $tokens[$i + 2]->id === T_STRING) {
$className = $tokens[$i + 2]->text;
$currentClass = $namespace !== '' ? $namespace . '\\' . $className : $className;
self::$classTraitUseDocs[$currentClass] = [];
$isClassLike = $token->id === T_CLASS || $token->id === T_INTERFACE || $token->id === T_TRAIT || (\defined('T_ENUM') && $token->id === T_ENUM);

if ($isClassLike) {
$isClassConst = false;
for ($prev = $i - 1; $prev >= 0; $prev--) {
if ($tokens[$prev]->id === T_WHITESPACE || $tokens[$prev]->id === T_COMMENT) {
continue;
}
if ($tokens[$prev]->id === T_DOUBLE_COLON) {
$isClassConst = true;
}

break;
}

if ($isClassConst) {
continue;
}

$nameToken = null;
for ($j = $i + 1; $j < $count; $j++) {
if ($tokens[$j]->id === T_WHITESPACE || $tokens[$j]->id === T_COMMENT || $tokens[$j]->id === T_DOC_COMMENT) {
continue;
}
if ($tokens[$j]->id === T_STRING) {
$nameToken = $tokens[$j];
}

break;
}

if ($nameToken !== null) {
$className = $namespace !== '' ? $namespace . '\\' . $nameToken->text : $nameToken->text;
self::$classTraitUseDocs[$className] = [];
$pendingClass = ['type' => 'named', 'name' => $className];
} elseif ($token->id === T_CLASS) {
$pendingClass = ['type' => 'anon', 'line' => $token->line];
}

continue;
}

if ($token->id === T_USE && $currentClass !== null) {
for ($k = $i - 1; $k >= 0; $k--) {
if ($tokens[$k]->id === T_DOC_COMMENT) {
self::$classTraitUseDocs[$currentClass][] = $tokens[$k]->text;
if ($token->text === '{' || $token->id === T_CURLY_OPEN || $token->id === T_DOLLAR_OPEN_CURLY_BRACES) {
$braceDepth++;
if ($pendingClass !== null) {
$classStack[] = [
'type' => $pendingClass['type'],
'name' => $pendingClass['name'] ?? null,
'line' => $pendingClass['line'] ?? null,
'depth' => $braceDepth,
];
$pendingClass = null;
}

break;
continue;
}

if ($token->text === '}') {
if ($classStack !== []) {
$top = end($classStack);
if ($braceDepth === $top['depth']) {
array_pop($classStack);
}
if ($tokens[$k]->id !== T_WHITESPACE) {
break;
}
$braceDepth--;

continue;
}

if ($token->id === T_USE && $classStack !== []) {
$isClosureUse = false;
for ($j = $i + 1; $j < $count; $j++) {
if ($tokens[$j]->id === T_WHITESPACE || $tokens[$j]->id === T_COMMENT) {
continue;
}
if ($tokens[$j]->text === '(') {
$isClosureUse = true;
}

break;
}

if (! $isClosureUse) {
for ($k = $i - 1; $k >= 0; $k--) {
if ($tokens[$k]->id === T_DOC_COMMENT) {
$top = end($classStack);
if ($top['type'] === 'named' && isset($top['name'])) {
self::$classTraitUseDocs[$top['name']][] = $tokens[$k]->text;
} elseif ($top['type'] === 'anon' && isset($top['line'])) {
self::$anonymousTraitUseDocs[$normalizedFile][$top['line']][] = $tokens[$k]->text;
}

break;
}
if ($tokens[$k]->id !== T_WHITESPACE) {
break;
}
}
}

continue;
}

if ($token->id === T_USE && $currentClass === null) {
if ($token->id === T_USE && $classStack === []) {
$useStatement = '';
for ($j = $i + 1; $j < $count; $j++) {
if ($tokens[$j]->text === ';') {
Expand Down Expand Up @@ -1189,9 +1342,9 @@ public static function parseFileMetadata(string $fileName, string $source): void
}
}

self::$fileUseImports[$fileName] = $imports;
self::$fileUseImports[$normalizedFile] = $imports;
} catch (\Throwable $e) {
self::$fileUseImports[$fileName] = [];
self::$fileUseImports[$normalizedFile] = [];
}
}
}
2 changes: 1 addition & 1 deletion src/Internal/Util/PathMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ public static function mayPathBeIncluded(string $normalizedPath): bool
}

if (str_starts_with($canon, 'var/')
|| (!str_starts_with($canon, '/var/') && str_contains($canon, '/var/'))
|| (! str_starts_with($canon, '/var/') && str_contains($canon, '/var/'))
|| (str_starts_with($canon, '/var/') && str_contains(substr($canon, 4), '/var/'))
) {
if (! self::hasIncludeMatchingPrefix('var/', $includes)) {
Expand Down
Loading
Loading