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: /**
54: * @var \PhpParser\Parser
55: */
56: private $phpParser;
57:
58: /**
59: * @var \PhpParser\NodeTraverser
60: */
61: private $nodeTraverser;
62:
63: /**
64: * @var \PhpParser\NodeVisitorAbstract
65: */
66: private $constantVisitor;
67:
68: public function __construct($astLocator = null, ?\PhpParser\Parser $phpParser = null)
69: {
70: $betterReflection = new BetterReflection();
71:
72: parent::__construct($astLocator ?? $betterReflection->astLocator());
73:
74: $this->phpParser = $phpParser ?? $betterReflection->phpParser();
75: $this->constantVisitor = $this->createConstantVisitor();
76:
77: $this->nodeTraverser = new NodeTraverser(new NameResolver(), $this->constantVisitor);
78: }
79:
80: /**
81: * {@inheritDoc}
82: *
83: * @throws InvalidArgumentException
84: * @throws InvalidFileLocation
85: */
86: protected function createLocatedSource(Identifier $identifier): ?\PHPStan\BetterReflection\SourceLocator\Located\LocatedSource
87: {
88: $locatedData = $this->attemptAutoloadForIdentifier($identifier);
89:
90: if ($locatedData === null) {
91: return null;
92: }
93:
94: if (! is_file($locatedData['fileName'])) {
95: return null;
96: }
97:
98: if (strtolower($identifier->getName()) !== strtolower($locatedData['name'])) {
99: return new AliasLocatedSource(file_get_contents($locatedData['fileName']), $locatedData['name'], $locatedData['fileName'], $identifier->getName());
100: }
101:
102: return new LocatedSource(file_get_contents($locatedData['fileName']), $identifier->getName(), $locatedData['fileName']);
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(static function () use ($className): ?string {
170: foreach (spl_autoload_functions() as $preExistingAutoloader) {
171: $preExistingAutoloader($className);
172:
173: /**
174: * This static variable is populated by the side-effect of the stream wrapper
175: * trying to read the file path when `include()` is used by an autoloader.
176: *
177: * This will not be `null` when the autoloader tried to read a file.
178: */
179: if (FileReadTrapStreamWrapper::$autoloadLocatedFile !== null) {
180: return FileReadTrapStreamWrapper::$autoloadLocatedFile;
181: }
182: }
183:
184: return null;
185: });
186:
187: if ($locatedFile === null) {
188: return null;
189: }
190:
191: return ['fileName' => $locatedFile, 'name' => $className];
192: } finally {
193: restore_error_handler();
194: }
195: }
196:
197: private function silenceErrors(): void
198: {
199: set_error_handler(static function () : bool {
200: return true;
201: });
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 ((($constNodeNamespacedName = $constNode->namespacedName) ? $constNodeNamespacedName->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: