1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Turbo;
4:
5: use Phar;
6: use PHPStanTurbo\Runtime;
7: use function class_exists;
8: use function dirname;
9: use function extension_loaded;
10: use function file_get_contents;
11: use function in_array;
12: use function is_file;
13: use function json_decode;
14: use function phpversion;
15: use function realpath;
16:
17: /**
18: * Activates the phpstan_turbo extension's shadowing classes.
19: *
20: * The extension registers nothing under PHPStan's class names at module
21: * startup. Once the Composer autoloader is registered, activateIfCompatible()
22: * checks the loaded extension's version against EXPECTED_EXTENSION_VERSION
23: * and, when it matches, asks Runtime::activateShadowing() to declare the
24: * native classes under the PHP twins' real names — PHPStan\TrinaryLogic then
25: * is the native class, linked like a PHP declaration (parents and interfaces
26: * resolved through the autoloader). A mismatched or absent extension never
27: * gets the call, and the PHP implementations load as usual.
28: *
29: * The version is the short SHA of the last commit touching turbo-ext/src/,
30: * enforced by the phar.yml turbo-version job; the native classes must
31: * behave exactly like the PHP implementations, hence the gate.
32: */
33: #[ReferencedByTurboExtension(key: 'turboExtensionEnabler')]
34: final class TurboExtensionEnabler
35: {
36:
37: public const EXPECTED_EXTENSION_VERSION = '1cb5faf';
38:
39: private static bool $active = false;
40:
41: private static bool $trustingOwnTypes = false;
42:
43: public static function isLoaded(): bool
44: {
45: return extension_loaded('phpstan_turbo');
46: }
47:
48: /**
49: * The version of the loaded extension when it does not pass the version
50: * gate. Null when the extension is not loaded or compatible.
51: */
52: public static function getIncompatibleLoadedVersion(): ?string
53: {
54: if (!self::isLoaded()) {
55: return null;
56: }
57:
58: $version = phpversion('phpstan_turbo');
59: if ($version === self::EXPECTED_EXTENSION_VERSION) {
60: return null;
61: }
62:
63: return $version === false ? 'unknown' : $version;
64: }
65:
66: /**
67: * The phar the running code is compiled from. Code compiled out of a phar
68: * carries the phar:// prefix of the path the phar was opened with, and
69: * Composer's bootstrap.php opens it through __DIR__, a resolved path. A
70: * path another program passes in, such as one with '..' in it, is
71: * resolved the same way, so that it matches that prefix.
72: */
73: private static function resolvePharPath(?string $pharPath): string
74: {
75: if ($pharPath === null) {
76: return Phar::running(false);
77: }
78:
79: $resolvedPath = realpath($pharPath);
80: if ($resolvedPath === false) {
81: return '';
82: }
83:
84: return $resolvedPath;
85: }
86:
87: private static function isCompatible(): bool
88: {
89: return self::isLoaded() && phpversion('phpstan_turbo') === self::EXPECTED_EXTENSION_VERSION;
90: }
91:
92: /**
93: * Whether activateIfCompatible() declared the native classes — the
94: * shadowed class names resolve to them only in that case.
95: */
96: public static function isActive(): bool
97: {
98: return self::$active;
99: }
100:
101: /**
102: * Read lazily by TypeCombinator, whose memoization lives in the native
103: * TypeCombinatorCache.
104: */
105: public static function isTypeCombinatorCacheEnabled(): bool
106: {
107: return self::$active;
108: }
109:
110: /**
111: * Must run after the Composer autoloader is registered (the native classes
112: * implement userland interfaces and may extend userland classes, resolved
113: * through it) and before anything could autoload one of the shadowed
114: * classes — a twin already declared cannot be shadowed.
115: *
116: * Another program that loads PHPStan's phar, Rector for example, passes
117: * the path to that phar: Phar::running() only knows the phar that runs.
118: *
119: * @api
120: */
121: public static function activateIfCompatible(?string $pharPath = null): void
122: {
123: if (!self::isCompatible()) {
124: return;
125: }
126:
127: // Both files are generated on composer dump-autoload by
128: // build/generate-turbo-manifest.php from the attributes; missing when
129: // the dump skipped scripts — run without the extension rather than
130: // fatal.
131: $root = dirname(__DIR__, 2);
132: $manifestFile = $root . '/vendor/turbo-shadowed-classes.json';
133: $classMapFile = $root . '/vendor/turbo-class-map.php';
134: if (!is_file($manifestFile) || !is_file($classMapFile)) {
135: return;
136: }
137:
138: $manifestContents = file_get_contents($manifestFile);
139: if ($manifestContents === false) {
140: return;
141: }
142:
143: // The manifest of shadowed pairs, from the ShadowedByTurboExtension
144: // attributes: each native class is declared with its twin's source
145: // file, so reflection keeps reading the PHP declaration.
146: /** @var array<string, array{php: string, cpp: string, vendored?: bool}> $manifest */
147: $manifest = json_decode($manifestContents, true);
148: $twinFiles = [];
149: foreach ($manifest as $className => $entry) {
150: $twinFiles[$className] = $root . '/' . $entry['php'];
151: }
152:
153: // Class names the native code references at run time, from the
154: // ReferencedByTurboExtension attributes, so a renamed class updates
155: // the map on the next autoloader dump.
156: Runtime::configure(require $classMapFile);
157:
158: Runtime::activateShadowing($twinFiles);
159:
160: // When running from a phar, arm the pthread_atfork hooks that keep
161: // phar:// reads safe in pcntl_fork()ed workers — libphar serves them
162: // through one shared archive fd whose seek cursor forked processes
163: // would otherwise race on. Fork mode requires the guard, and
164: // ForkParallelChecker only allows fork with the extension active.
165: if (class_exists('Phar', false)) {
166: $pharPath = self::resolvePharPath($pharPath);
167: if ($pharPath !== '') {
168: Runtime::enablePharForkGuard($pharPath);
169: }
170: }
171:
172: self::$active = true;
173: }
174:
175: /**
176: * Whether the extension drops the engine's argument and return type
177: * checks from PHPStan's own code in this process, see
178: * trustOwnTypesIfSuitable().
179: */
180: public static function isTrustingOwnTypes(): bool
181: {
182: return self::$trustingOwnTypes;
183: }
184:
185: /**
186: * PHPStan's code is verified by PHPStan itself at the strictest level, so
187: * the engine's run-time checks of its parameter and return types re-check
188: * what analysis already proved — at about 8% of the analysis CPU: a
189: * class-typed parameter costs a class lookup and an instanceof on every
190: * call, a typed return the same on the way out. With the extension active
191: * and PHPStan running from a phar, its optimizer pass (TrustedTypes.cpp)
192: * drops those checks from the code compiled out of the phar. Nothing else
193: * is touched: extensions, bootstrap files and the analysed project keep
194: * their checks, including on what they receive from PHPStan and return to
195: * it — a check sits in the callee.
196: *
197: * What is lost is the TypeError at the boundary when such code passes a
198: * wrong value into PHPStan: it surfaces later, deeper. That is why --debug
199: * keeps the checks — the "run with --debug" advice on internal errors then
200: * yields the original error. PHPUnit never gets here, so the test suites
201: * of PHPStan and of extensions always run fully checked.
202: *
203: * Must run before the Composer autoloader and preload.php are compiled:
204: * the pass rewrites scripts as they are compiled, so whatever was
205: * compiled earlier keeps its checks. Independent of activateIfCompatible()
206: * — only the version gate matters here.
207: *
208: * Another program that loads PHPStan's phar, Rector for example, passes
209: * the path to that phar: Phar::running() only knows the phar that runs.
210: *
211: * @api
212: * @param list<string> $argv
213: */
214: public static function trustOwnTypesIfSuitable(array $argv, ?string $pharPath = null): void
215: {
216: if (!self::isCompatible()) {
217: return;
218: }
219: if (in_array('--debug', $argv, true)) {
220: return;
221: }
222: if (!class_exists('Phar', false)) {
223: return;
224: }
225: $pharPath = self::resolvePharPath($pharPath);
226: if ($pharPath === '') {
227: return;
228: }
229:
230: self::$trustingOwnTypes = Runtime::trustTypesUnder('phar://' . $pharPath . '/');
231: }
232:
233: }
234: