Files
XC_VM/tools/phpstan/gen-constants-stub.php
T

116 lines
4.2 KiB
PHP
Raw Normal View History

<?php
/**
* Generates tools/phpstan/constants.stub.php by scanning every define() call in
* src/ and inferring a rough type for each constant. Run from the project root:
*
* php tools/phpstan/gen-constants-stub.php > tools/phpstan/constants.stub.php
*
* @package XC_VM
*/
$root = dirname(__DIR__, 2) . '/src';
$dir = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS));
$consts = [];
foreach ($dir as $f) {
if ($f->getExtension() !== 'php') {
continue;
}
$src = @file_get_contents($f->getPathname());
if ($src === false || strpos($src, 'define(') === false) {
continue;
}
$tokens = token_get_all($src);
$n = count($tokens);
for ($i = 0; $i < $n; $i++) {
$t = $tokens[$i];
if (!(is_array($t) && $t[0] === T_STRING && $t[1] === 'define')) {
continue;
}
$name = null;
$k = $i + 1;
for (; $k < $n && $k < $i + 40; $k++) {
$tk = $tokens[$k];
if (is_array($tk) && $tk[0] === T_CONSTANT_ENCAPSED_STRING) {
$name = trim($tk[1], "'\"");
break;
}
if ($tk === ')') {
break;
}
}
if (!$name || !preg_match('/^[A-Z_][A-Z0-9_]*$/', $name) || isset($consts[$name])) {
continue;
}
$hasIntLit = $hasStrLit = $hasConcat = $hasBool = false;
$isIntOnly = true;
$seen = false;
$d = 0;
for ($m = $k + 1; $m < $n && $m < $k + 80; $m++) {
$tm = $tokens[$m];
if ($tm === '(') { $d++; continue; }
if ($tm === ')') { if ($d === 0) break; $d--; continue; }
if ($tm === ',' && $d === 0) { $seen = true; continue; }
if (!$seen) { continue; }
if (is_array($tm)) {
if ($tm[0] === T_LNUMBER || $tm[0] === T_DNUMBER) { $hasIntLit = true; }
elseif ($tm[0] === T_CONSTANT_ENCAPSED_STRING) { $hasStrLit = true; $isIntOnly = false; }
elseif ($tm[0] === T_STRING && in_array(strtolower($tm[1]), ['true', 'false'], true)) { $hasBool = true; $isIntOnly = false; }
elseif ($tm[0] === T_WHITESPACE) { /* skip */ }
else { $isIntOnly = false; }
} else {
if ($tm === '.') { $hasConcat = true; $isIntOnly = false; }
else { $isIntOnly = false; }
}
}
if ($hasConcat || $hasStrLit) { $type = 'string'; }
elseif ($hasBool && !$hasIntLit) { $type = 'bool'; }
elseif ($hasIntLit && $isIntOnly) { $type = 'int'; }
else { $type = 'mixed'; }
$consts[$name] = $type;
}
}
// Explicit type overrides for constants whose value the heuristic cannot
// classify (defined from variables or function calls). Keeps the stub accurate
// and prevents false null/mixed positives at call sites.
$overrides = [
'HOST' => 'string', // trim(explode(':', HTTP_HOST)[0])
'PAGE_NAME' => 'string',
'PHP_ERRORS' => 'bool', // dev-mode flag
];
foreach ($overrides as $name => $type) {
if (isset($consts[$name])) {
$consts[$name] = $type;
}
}
ksort($consts);
echo "<?php\n\n";
echo "/**\n";
echo " * PHPStan constants stub for XC_VM (auto-generated by gen-constants-stub.php).\n";
echo " *\n";
echo " * The application defines these constants at RUNTIME inside methods, where\n";
echo " * PHPStan cannot see them via static scanning. Values are a cast of an unset\n";
echo " * env var so PHPStan gets the correct GENERAL type without narrowing to a\n";
echo " * literal (which would cause false always-true/false comparison reports).\n";
echo " *\n";
echo " * @package XC_VM\n";
echo " */\n\n";
foreach ($consts as $name => $type) {
// Use mt_rand()-based expressions so PHPStan infers a GENERAL type
// (int/string/bool) and never constant-folds to a literal (0/'') — a
// literal would create false positives like division-by-zero or
// always-false comparisons at call sites.
$expr = match ($type) {
'string' => "(string) mt_rand()",
'int' => "mt_rand()",
'bool' => "(bool) mt_rand()",
default => "json_decode((string) mt_rand(), true)",
};
echo "if (!defined('$name')) define('$name', $expr);\n";
}