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