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