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