1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\SourceLocator\Type;
6:
7: use InvalidArgumentException;
8: use PhpParser\Node;
9: use PhpParser\NodeTraverser;
10: use PhpParser\NodeVisitor;
11: use PhpParser\NodeVisitor\NameResolver;
12: use PhpParser\NodeVisitorAbstract;
13: use PhpParser\Parser;
14: use ReflectionClass;
15: use ReflectionException;
16: use ReflectionFunction;
17: use PHPStan\BetterReflection\BetterReflection;
18: use PHPStan\BetterReflection\Identifier\Identifier;
19: use PHPStan\BetterReflection\Reflection\Exception\InvalidConstantNode;
20: use PHPStan\BetterReflection\SourceLocator\Ast\Locator as AstLocator;
21: use PHPStan\BetterReflection\SourceLocator\Exception\InvalidFileLocation;
22: use PHPStan\BetterReflection\SourceLocator\FileChecker;
23: use PHPStan\BetterReflection\SourceLocator\Located\AliasLocatedSource;
24: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
25: use PHPStan\BetterReflection\SourceLocator\Type\AutoloadSourceLocator\FileReadTrapStreamWrapper;
26: use PHPStan\BetterReflection\Util\ClassExistenceChecker;
27: use PHPStan\BetterReflection\Util\ConstantNodeChecker;
28:
29: use function array_key_exists;
30: use function array_reverse;
31: use function assert;
32: use function defined;
33: use function file_get_contents;
34: use function function_exists;
35: use function get_defined_constants;
36: use function get_included_files;
37: use function is_file;
38: use function is_string;
39: use function restore_error_handler;
40: use function set_error_handler;
41: use function spl_autoload_functions;
42: use function strtolower;
43:
44: /**
45: * Use PHP's built in autoloader to locate a class, without actually loading.
46: *
47: * There are some prerequisites...
48: * - we expect the autoloader to load classes from a file (i.e. using require/include)
49: * - your autoloader of choice does not replace stream wrappers
50: */
51: class AutoloadSourceLocator extends AbstractSourceLocator
52: {
53: private Parser $phpParser;
54:
55: private NodeTraverser $nodeTraverser;
56:
57: private NodeVisitorAbstract $constantVisitor;
58:
59: public function __construct($astLocator = null, ?\PhpParser\Parser $phpParser = null)
60: {
61: $betterReflection = new BetterReflection();
62:
63: parent::__construct($astLocator ?? $betterReflection->astLocator());
64:
65: $this->phpParser = $phpParser ?? $betterReflection->phpParser();
66: $this->constantVisitor = $this->createConstantVisitor();
67:
68: $this->nodeTraverser = new NodeTraverser(new NameResolver(), $this->constantVisitor);
69: }
70:
71: /**
72: * {@inheritDoc}
73: *
74: * @throws InvalidArgumentException
75: * @throws InvalidFileLocation
76: */
77: protected function createLocatedSource(Identifier $identifier): ?\PHPStan\BetterReflection\SourceLocator\Located\LocatedSource
78: {
79: $locatedData = $this->attemptAutoloadForIdentifier($identifier);
80:
81: if ($locatedData === null) {
82: return null;
83: }
84:
85: if (! is_file($locatedData['fileName'])) {
86: return null;
87: }
88:
89: $fileContents = file_get_contents($locatedData['fileName']);
90: assert($fileContents !== false);
91:
92: if (strtolower($identifier->getName()) !== strtolower($locatedData['name'])) {
93: return new AliasLocatedSource(
94: $fileContents,
95: $locatedData['name'],
96: $locatedData['fileName'],
97: $identifier->getName(),
98: );
99: }
100:
101: return new LocatedSource(
102: $fileContents,
103: $identifier->getName(),
104: $locatedData['fileName'],
105: );
106: }
107:
108: /**
109: * Attempts to locate the specified identifier.
110: *
111: * @return array{fileName: string, name: string}|null
112: *
113: * @throws ReflectionException
114: */
115: private function attemptAutoloadForIdentifier(Identifier $identifier): ?array
116: {
117: if ($identifier->isClass()) {
118: return $this->locateClassByName($identifier->getName());
119: }
120:
121: if ($identifier->isFunction()) {
122: return $this->locateFunctionByName($identifier->getName());
123: }
124:
125: if ($identifier->isConstant()) {
126: return $this->locateConstantByName($identifier->getName());
127: }
128:
129: return null;
130: }
131:
132: /**
133: * Attempt to locate a class by name.
134: *
135: * If class already exists, simply use internal reflection API to get the
136: * filename and store it.
137: *
138: * If class does not exist, we make an assumption that whatever autoloaders
139: * that are registered will be loading a file. We then override the file://
140: * protocol stream wrapper to "capture" the filename we expect the class to
141: * be in, and then restore it. Note that class_exists will cause an error
142: * that it cannot find the file, so we squelch the errors by overriding the
143: * error handler temporarily.
144: *
145: * Note: the following code is designed so that the first hit on an actual
146: * **file** leads to a path being resolved. No actual autoloading nor
147: * file reading should happen, and most certainly no other classes
148: * should exist after execution. The only filesystem access is to
149: * check whether the file exists.
150: *
151: * @return array{fileName: string, name: string}|null
152: *
153: * @throws ReflectionException
154: */
155: private function locateClassByName(string $className): ?array
156: {
157: if (ClassExistenceChecker::exists($className, false)) {
158: $classReflection = new ReflectionClass($className);
159:
160: $filename = $classReflection->getFileName();
161:
162: if (! is_string($filename)) {
163: return null;
164: }
165:
166: return ['fileName' => $filename, 'name' => $classReflection->getName()];
167: }
168:
169: $this->silenceErrors();
170:
171: try {
172: $locatedFile = FileReadTrapStreamWrapper::withStreamWrapperOverride(
173: static function () use ($className): ?string {
174: foreach (spl_autoload_functions() as $preExistingAutoloader) {
175: $preExistingAutoloader($className);
176:
177: /**
178: * This static variable is populated by the side-effect of the stream wrapper
179: * trying to read the file path when `include()` is used by an autoloader.
180: *
181: * This will not be `null` when the autoloader tried to read a file.
182: */
183: if (FileReadTrapStreamWrapper::$autoloadLocatedFile !== null) {
184: return FileReadTrapStreamWrapper::$autoloadLocatedFile;
185: }
186: }
187:
188: return null;
189: },
190: );
191:
192: if ($locatedFile === null) {
193: return null;
194: }
195:
196: return ['fileName' => $locatedFile, 'name' => $className];
197: } finally {
198: restore_error_handler();
199: }
200: }
201:
202: private function silenceErrors(): void
203: {
204: set_error_handler(static fn (): bool => true);
205: }
206:
207: /**
208: * We can only load functions if they already exist, because PHP does not
209: * have function autoloading. Therefore if it exists, we simply use the
210: * internal reflection API to find the filename. If it doesn't we can do
211: * nothing so throw an exception.
212: *
213: * @return array{fileName: string, name: string}|null
214: *
215: * @throws ReflectionException
216: */
217: private function locateFunctionByName(string $functionName): ?array
218: {
219: if (! function_exists($functionName)) {
220: return null;
221: }
222:
223: $reflectionFileName = (new ReflectionFunction($functionName))->getFileName();
224:
225: if (! is_string($reflectionFileName)) {
226: return null;
227: }
228:
229: return ['fileName' => $reflectionFileName, 'name' => $functionName];
230: }
231:
232: /**
233: * We can only load constants if they already exist, because PHP does not
234: * have constant autoloading. Therefore if it exists, we simply use brute force
235: * to search throughout all included files to find the right filename.
236: *
237: * @return array{fileName: string, name: string}|null
238: */
239: private function locateConstantByName(string $constantName): ?array
240: {
241: if (! defined($constantName)) {
242: return null;
243: }
244:
245: /** @var array<string, array<string, scalar|list<scalar>|resource|null>> $constants */
246: $constants = get_defined_constants(true);
247:
248: if (! array_key_exists($constantName, $constants['user'])) {
249: return null;
250: }
251:
252: /**
253: * @psalm-suppress UndefinedMethod
254: * @phpstan-ignore method.notFound
255: */
256: $this->constantVisitor->setConstantName($constantName);
257:
258: $constantFileName = null;
259:
260: // Note: looking at files in reverse order, since newer files are more likely to have
261: // defined a constant that is being looked up. Earlier files are possibly related
262: // to libraries/frameworks that we rely upon.
263: // @infection-ignore-all UnwrapArrayReverse: Ignore because the result is some with or without array_reverse()
264: /** @phpstan-var non-empty-string $includedFileName */
265: foreach (array_reverse(get_included_files()) as $includedFileName) {
266: try {
267: FileChecker::assertReadableFile($includedFileName);
268: } catch (InvalidFileLocation $exception) {
269: continue;
270: }
271:
272: $fileContents = file_get_contents($includedFileName);
273: assert($fileContents !== false);
274:
275: /** @var list<Node\Stmt> $ast */
276: $ast = $this->phpParser->parse($fileContents);
277:
278: $this->nodeTraverser->traverse($ast);
279:
280: /**
281: * @psalm-suppress UndefinedMethod
282: * @phpstan-ignore method.notFound
283: */
284: if ($this->constantVisitor->getNode() !== null) {
285: $constantFileName = $includedFileName;
286: break;
287: }
288: }
289:
290: if ($constantFileName === null) {
291: return null;
292: }
293:
294: return ['fileName' => $constantFileName, 'name' => $constantName];
295: }
296:
297: private function createConstantVisitor(): NodeVisitorAbstract
298: {
299: return new class () extends NodeVisitorAbstract
300: {
301: /**
302: * @var string|null
303: */
304: private $constantName = null;
305:
306: /**
307: * @var \PhpParser\Node\Stmt\Const_|\PhpParser\Node\Expr\FuncCall|null
308: */
309: private $node = null;
310:
311: public function enterNode(Node $node): ?int
312: {
313: if ($node instanceof Node\Stmt\Const_) {
314: foreach ($node->consts as $constNode) {
315: if ((($nullsafeVariable1 = $constNode->namespacedName) ? $nullsafeVariable1->toString() : null) === $this->constantName) {
316: $this->node = $node;
317:
318: return NodeVisitor::STOP_TRAVERSAL;
319: }
320: }
321:
322: return NodeVisitor::DONT_TRAVERSE_CHILDREN;
323: }
324:
325: if ($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\FuncCall) {
326: $functionCall = $node->expr;
327:
328: try {
329: ConstantNodeChecker::assertValidDefineFunctionCall($functionCall);
330: } catch (InvalidConstantNode $exception) {
331: return null;
332: }
333:
334: $argumentNameNode = $functionCall->args[0];
335: assert($argumentNameNode instanceof Node\Arg);
336: $nameNode = $argumentNameNode->value;
337: assert($nameNode instanceof Node\Scalar\String_);
338:
339: if ($nameNode->value === $this->constantName) {
340: $this->node = $functionCall;
341:
342: return NodeVisitor::STOP_TRAVERSAL;
343: }
344: }
345:
346: return null;
347: }
348:
349: public function setConstantName(string $constantName): void
350: {
351: $this->constantName = $constantName;
352: }
353:
354: /** @return Node\Stmt\Const_|Node\Expr\FuncCall|null */
355: public function getNode()
356: {
357: return $this->node;
358: }
359: };
360: }
361: }
362: