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: /** @phpstan-var non-empty-string $includedFileName */
260: foreach (array_reverse(get_included_files()) as $includedFileName) {
261: try {
262: FileChecker::assertReadableFile($includedFileName);
263: } catch (InvalidFileLocation $exception) {
264: continue;
265: }
266:
267: /** @var list<Node\Stmt> $ast */
268: $ast = $this->phpParser->parse(file_get_contents($includedFileName));
269:
270: $this->nodeTraverser->traverse($ast);
271:
272: /** @psalm-suppress UndefinedMethod */
273: if ($this->constantVisitor->getNode() !== null) {
274: $constantFileName = $includedFileName;
275: break;
276: }
277: }
278:
279: if ($constantFileName === null) {
280: return null;
281: }
282:
283: return ['fileName' => $constantFileName, 'name' => $constantName];
284: }
285:
286: private function createConstantVisitor(): NodeVisitorAbstract
287: {
288: return new class () extends NodeVisitorAbstract
289: {
290: /**
291: * @var string|null
292: */
293: private $constantName = null;
294:
295: /**
296: * @var \PhpParser\Node\Stmt\Const_|\PhpParser\Node\Expr\FuncCall|null
297: */
298: private $node = null;
299:
300: public function enterNode(Node $node): ?int
301: {
302: if ($node instanceof Node\Stmt\Const_) {
303: foreach ($node->consts as $constNode) {
304: if ((($constNodeNamespacedName = $constNode->namespacedName) ? $constNodeNamespacedName->toString() : null) === $this->constantName) {
305: $this->node = $node;
306:
307: return NodeTraverser::STOP_TRAVERSAL;
308: }
309: }
310:
311: return NodeTraverser::DONT_TRAVERSE_CHILDREN;
312: }
313:
314: if ($node instanceof Node\Expr\FuncCall) {
315: try {
316: /** @psalm-suppress InternalClass, InternalMethod */
317: ConstantNodeChecker::assertValidDefineFunctionCall($node);
318: } catch (InvalidConstantNode $exception) {
319: return null;
320: }
321:
322: $argumentNameNode = $node->args[0];
323: assert($argumentNameNode instanceof Node\Arg);
324: $nameNode = $argumentNameNode->value;
325: assert($nameNode instanceof Node\Scalar\String_);
326:
327: if ($nameNode->value === $this->constantName) {
328: $this->node = $node;
329:
330: return NodeTraverser::STOP_TRAVERSAL;
331: }
332: }
333:
334: return null;
335: }
336:
337: public function setConstantName(string $constantName): void
338: {
339: $this->constantName = $constantName;
340: }
341:
342: /** @return Node\Stmt\Const_|Node\Expr\FuncCall|null */
343: public function getNode()
344: {
345: return $this->node;
346: }
347: };
348: }
349: }
350: