From eccf5a0ef18b560a45bfb4457f283c809a5a496a Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 27 Aug 2026 18:38:59 +0800 Subject: [PATCH] fix(compiler): enforce AST type boundaries --- composer.json | 2 +- phpstan-bootstrap.php | 15 + phpstan.neon | 17 + src/CompilerBase.php | 385 ++++++++------------ src/Diagnostics/CompilerDiagnosticTrait.php | 7 +- src/Installer/LinuxPackageManager.php | 2 + src/NativeClass/NativeClassSupportTrait.php | 2 +- src/Parser/AssignOpTrait.php | 2 + src/Parser/AstNodeType.php | 91 +++-- src/Parser/StdContainerTrait.php | 1 + src/Preprocessor.php | 114 +++--- src/Translator.php | 92 ++--- 12 files changed, 324 insertions(+), 406 deletions(-) create mode 100644 phpstan-bootstrap.php create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index 385e1624..3a617091 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "scripts": { "cs-fix": "php-cs-fixer fix $1", - "analyse": "phpstan analyse --memory-limit 512M -l 5 -c phpstan.neon", + "analyse": "phpstan analyse --memory-limit 1G -c phpstan.neon", "rector": "rector process --clear-cache" }, "suggest": { diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php new file mode 100644 index 00000000..a9589cc9 --- /dev/null +++ b/phpstan-bootstrap.php @@ -0,0 +1,15 @@ +name->toString(), 'true') === 0 ? self::VALUE_TRUE : self::VALUE_FALSE; + } public const string LITERAL_STRINGS = '_literal_strings'; public const string LITERAL_STRING_GETTER = 'get_str'; public const string ANON_CLASS = '_anon_class_'; @@ -576,7 +581,7 @@ class CompilerBase implements PropertyAccessContext return $this->lang; } - protected function unsupportedSyntax(NodeAbstract $node): never + protected function unsupportedSyntax(Node $node): never { $message = 'Error: Unsupported ' . $this->getLang() . ' Syntax,'; $message .= ' Line: ' . $this->getLine($node) . ', Type: ' . $this->getType($node) . PHP_EOL; @@ -731,7 +736,7 @@ class CompilerBase implements PropertyAccessContext return ''; } - public function parseExpr(NodeAbstract $expr): string + public function parseExpr(Node $expr): string { if ($expr->hasAttribute('replace')) { return $expr->getAttribute('replace'); @@ -744,217 +749,114 @@ class CompilerBase implements PropertyAccessContext if ($expr instanceof Node\Expr\BinaryOp) { $this->assertNativeObjectBinaryOperatorSupported($expr); } - switch ($type) { - case 'Expr_Isset': - return $this->parseIsset($expr); - case 'Expr_Empty': - return $this->parseEmpty($expr); - case 'Expr_Assign': - return $this->parseAssign($expr); - case 'Expr_AssignRef': - return $this->parseAssignRef($expr); - case 'Expr_Print': - return $this->parsePrint($expr); - case 'Expr_BinaryOp_Equal': - return $this->parseBinaryOpEqual($expr); - case 'Expr_BinaryOp_NotEqual': - return $this->parseBinaryOpNotEqual($expr); - case 'Expr_BinaryOp_Identical': - return $this->parseBinaryOpIdentical($expr); - case 'Expr_BinaryOp_NotIdentical': - return $this->parseBinaryOpNotIdentical($expr); - case 'Expr_BooleanNot': - return $this->parseBooleanNot($expr); - case 'Expr_BinaryOp_Plus': - return $this->parseBinaryOpPlus($expr); - case 'Expr_BinaryOp_Div': - return $this->parseBinaryOpDiv($expr); - case 'Expr_BinaryOp_Smaller': - return $this->parseBinaryOpSmaller($expr); - case 'Expr_BinaryOp_SmallerOrEqual': - return $this->parseBinaryOpSmallerOrEqual($expr); - case 'Expr_BinaryOp_GreaterOrEqual': - return $this->parseBinaryOpGreaterOrEqual($expr); - case 'Expr_BinaryOp_Spaceship': - return $this->parseBinaryOpSpaceship($expr); - case 'Expr_BinaryOp_Coalesce': - return $this->parseBinaryOpCoalesce($expr); - case 'Expr_PreInc': - return $this->parsePreInc($expr); - case 'Expr_PostInc': - return $this->parsePostInc($expr); - case 'Expr_PreDec': - return $this->parsePreDec($expr); - case 'Expr_PostDec': - return $this->parsePostDec($expr); - case 'Expr_AssignOp_Plus': - return $this->parseAssignOpPlus($expr); - case 'Expr_AssignOp_Minus': - return $this->parseAssignOpMinus($expr); - case 'Expr_AssignOp_Mul': - return $this->parseAssignOpMul($expr); - case 'Expr_AssignOp_Div': - return $this->parseAssignOpDiv($expr); - case 'Expr_AssignOp_Mod': - return $this->parseAssignOpMod($expr); - case 'Expr_AssignOp_Concat': - return $this->parseAssignOpConcat($expr); - case 'Expr_AssignOp_ShiftLeft': - return $this->parseAssignOpShiftLeft($expr); - case 'Expr_AssignOp_ShiftRight': - return $this->parseAssignOpShiftRight($expr); - case 'Expr_AssignOp_BitwiseAnd': - return $this->parseAssignOpBitwiseAnd($expr); - case 'Expr_AssignOp_BitwiseOr': - return $this->parseAssignOpBitwiseOr($expr); - case 'Expr_AssignOp_BitwiseXor': - return $this->parseAssignOpBitwiseXor($expr); - case 'Expr_AssignOp_Pow': - return $this->parseAssignOpPow($expr); - case 'Expr_AssignOp_Coalesce': - return $this->parseAssignOpCoalesce($expr); - case 'Expr_BinaryOp_Mul': - return $this->parseBinaryOpMul($expr); - case 'Expr_BinaryOp_Concat': - return $this->parseBinaryOpConcat($expr); - case 'Expr_BinaryOp_Greater': - return $this->parseBinaryOpGreater($expr); - case 'Expr_BinaryOp_LogicalAnd': - case 'Expr_BinaryOp_BooleanAnd': - return $this->parseBinaryOpLogicalAnd($expr); - case 'Expr_BinaryOp_LogicalOr': - case 'Expr_BinaryOp_BooleanOr': - return $this->parseBinaryOpLogicalOr($expr); - case 'Expr_BinaryOp_LogicalXor': - return $this->parseBinaryOpLogicalXor($expr); - case 'Expr_BinaryOp_Minus': - return $this->parseBinaryOpMinus($expr); - case 'Expr_Array': - return $this->parseArray($expr); - case 'Expr_ArrayDimFetch': - return $this->parseArrayDimFetch($expr); - case 'Expr_PropertyFetch': - return $this->parsePropertyFetch($expr); - case 'Expr_NullsafePropertyFetch': - return $this->parseNullsafePropertyFetch($expr); - case 'Expr_NullsafeMethodCall': - return $this->parseNullsafeMethodCall($expr); - case 'Expr_BinaryOp_ShiftLeft': - return $this->parseBinaryOpShiftLeft($expr); - case 'Expr_BinaryOp_ShiftRight': - return $this->parseBinaryOpShiftRight($expr); - case 'Expr_BinaryOp_BitwiseAnd': - return $this->parseBinaryOpBitwiseAnd($expr); - case 'Expr_BinaryOp_BitwiseOr': - return $this->parseBinaryOpBitwiseOr($expr); - case 'Expr_BinaryOp_BitwiseXor': - return $this->parseBinaryOpBitwiseXor($expr); - case 'Expr_BinaryOp_Pipe': - return $this->parsePipeOperator($expr); - case 'Expr_BitwiseNot': - return $this->parseBitwiseNot($expr); - case 'Expr_BinaryOp_Mod': - return $this->parseBinaryOpMod($expr); - case 'Expr_BinaryOp_Pow': - return $this->parseBinaryOpPow($expr); - case 'Expr_Ternary': - return $this->parseTernary($expr); - case 'Expr_Match': - return $this->parseMatch($expr); - case 'Expr_FuncCall': - return $this->parseFuncCall($expr); - case 'Expr_MethodCall': - return $this->parseMethodCall($expr); - case 'Expr_StaticCall': - return $this->parseStaticCall($expr); - case 'Expr_StaticPropertyFetch': - return $this->parseStaticPropertyFetch($expr); - case 'Expr_ClassConstFetch': - return $this->parseClassConstFetch($expr); - case 'Expr_Include': - return $this->parseInclude($expr); - case 'Expr_Eval': - return $this->parseEval($expr); - case 'Expr_New': - return $this->parseNew($expr); - case 'Expr_Clone': - return $this->parseClone($expr); - case 'Expr_Instanceof': - return $this->parseInstanceof($expr); - case 'Expr_Throw': - return $this->parseThrow($expr); - case 'Expr_ShellExec': - return $this->parseShellExec($expr); - case 'Expr_Closure': - return $this->parseClosure($expr); - case 'Expr_ArrowFunction': - return $this->parseArrowFunction($expr); - case 'Name_FullyQualified': - return $this->parseFullyQualifiedName($expr); - case 'Scalar_Int': - case 'Scalar_Float': - case 'Scalar_String': - return $this->parseIdentifier($expr); - case 'Expr_Variable': - $varName = $this->parseIdentifier($expr); - $this->requireVar($expr, $varName); - if ($this->isStdContainer($varName)) { - return $varName . '_ref'; - } - // $GLOBALS is an INDIRECT to &EG(symbol_table), - // whose refcount MUST NOT be directly manipulated. - // Use php::globalsArray() to create a separated copy. - if ($varName === 'GLOBALS') { - return 'php::globalsArray()'; - } - return $varName; - case 'Scalar_MagicConst_File': - case 'Scalar_MagicConst_Dir': - case 'Scalar_MagicConst_Line': - case 'Scalar_MagicConst_Function': - case 'Scalar_MagicConst_Method': - case 'Scalar_MagicConst_Class': - case 'Scalar_MagicConst_Trait': - case 'Scalar_MagicConst_Namespace': - case 'Scalar_MagicConst_Property': - return $this->parseMagicConst($expr); - case 'Scalar_InterpolatedString': - return $this->parseInterpolatedString($expr); - case 'Expr_Cast_Int': - return $this->parseCastInt($expr); - case 'Expr_Cast_Double': - return $this->parseCastDouble($expr); - case 'Expr_Cast_Bool': - return $this->parseCastBool($expr); - case 'Expr_Cast_String': - return $this->parseCastString($expr); - case 'Expr_Cast_Array': - return $this->parseCastArray($expr); - case 'Expr_Cast_Object': - return $this->parseCastObject($expr); - case 'Expr_Cast_Void': - return $this->parseCastVoid($expr); - case 'Expr_ConstFetch': - return $this->parseConstFetch($expr); - case 'Expr_UnaryMinus': - return $this->parseUnaryMinus($expr); - case 'Expr_UnaryPlus': - return $this->parseUnaryPlus($expr); - case 'InterpolatedStringPart': - return $this->parseInterpolatedStringPart($expr); - case 'Expr_ErrorSuppress': - return $this->parseErrorSuppress($expr); - case 'Expr_Exit': - return $this->parseExit($expr); - case 'Expr_Yield': - return $this->parseYieldExpr($expr); - case 'Expr_YieldFrom': - return $this->parseYieldFromExpr($expr); - default: - $this->unsupportedSyntax($expr); - break; - } - return ''; + if ($expr instanceof Expr\Variable) { + $varName = $this->parseIdentifier($expr); + $this->requireVar($expr, $varName); + if ($this->isStdContainer($varName)) { + return $varName . '_ref'; + } + // $GLOBALS is an INDIRECT to &EG(symbol_table), whose refcount + // MUST NOT be directly manipulated. Create a separated copy. + return $varName === 'GLOBALS' ? 'php::globalsArray()' : $varName; + } + + return match (true) { + $expr instanceof Expr\Isset_ => $this->parseIsset($expr), + $expr instanceof Expr\Empty_ => $this->parseEmpty($expr), + $expr instanceof Expr\Assign => $this->parseAssign($expr), + $expr instanceof Expr\AssignRef => $this->parseAssignRef($expr), + $expr instanceof Expr\Print_ => $this->parsePrint($expr), + $expr instanceof Expr\BinaryOp\Equal => $this->parseBinaryOpEqual($expr), + $expr instanceof Expr\BinaryOp\NotEqual => $this->parseBinaryOpNotEqual($expr), + $expr instanceof Expr\BinaryOp\Identical => $this->parseBinaryOpIdentical($expr), + $expr instanceof Expr\BinaryOp\NotIdentical => $this->parseBinaryOpNotIdentical($expr), + $expr instanceof Expr\BooleanNot => $this->parseBooleanNot($expr), + $expr instanceof Expr\BinaryOp\Plus => $this->parseBinaryOpPlus($expr), + $expr instanceof Expr\BinaryOp\Div => $this->parseBinaryOpDiv($expr), + $expr instanceof Expr\BinaryOp\Smaller => $this->parseBinaryOpSmaller($expr), + $expr instanceof Expr\BinaryOp\SmallerOrEqual => $this->parseBinaryOpSmallerOrEqual($expr), + $expr instanceof Expr\BinaryOp\GreaterOrEqual => $this->parseBinaryOpGreaterOrEqual($expr), + $expr instanceof Expr\BinaryOp\Spaceship => $this->parseBinaryOpSpaceship($expr), + $expr instanceof Expr\BinaryOp\Coalesce => $this->parseBinaryOpCoalesce($expr), + $expr instanceof Expr\PreInc => $this->parsePreInc($expr), + $expr instanceof Expr\PostInc => $this->parsePostInc($expr), + $expr instanceof Expr\PreDec => $this->parsePreDec($expr), + $expr instanceof Expr\PostDec => $this->parsePostDec($expr), + $expr instanceof Expr\AssignOp\Plus => $this->parseAssignOpPlus($expr), + $expr instanceof Expr\AssignOp\Minus => $this->parseAssignOpMinus($expr), + $expr instanceof Expr\AssignOp\Mul => $this->parseAssignOpMul($expr), + $expr instanceof Expr\AssignOp\Div => $this->parseAssignOpDiv($expr), + $expr instanceof Expr\AssignOp\Mod => $this->parseAssignOpMod($expr), + $expr instanceof Expr\AssignOp\Concat => $this->parseAssignOpConcat($expr), + $expr instanceof Expr\AssignOp\ShiftLeft => $this->parseAssignOpShiftLeft($expr), + $expr instanceof Expr\AssignOp\ShiftRight => $this->parseAssignOpShiftRight($expr), + $expr instanceof Expr\AssignOp\BitwiseAnd => $this->parseAssignOpBitwiseAnd($expr), + $expr instanceof Expr\AssignOp\BitwiseOr => $this->parseAssignOpBitwiseOr($expr), + $expr instanceof Expr\AssignOp\BitwiseXor => $this->parseAssignOpBitwiseXor($expr), + $expr instanceof Expr\AssignOp\Pow => $this->parseAssignOpPow($expr), + $expr instanceof Expr\AssignOp\Coalesce => $this->parseAssignOpCoalesce($expr), + $expr instanceof Expr\BinaryOp\Mul => $this->parseBinaryOpMul($expr), + $expr instanceof Expr\BinaryOp\Concat => $this->parseBinaryOpConcat($expr), + $expr instanceof Expr\BinaryOp\Greater => $this->parseBinaryOpGreater($expr), + $expr instanceof Expr\BinaryOp\LogicalAnd, + $expr instanceof Expr\BinaryOp\BooleanAnd => $this->parseBinaryOpLogicalAnd($expr), + $expr instanceof Expr\BinaryOp\LogicalOr, + $expr instanceof Expr\BinaryOp\BooleanOr => $this->parseBinaryOpLogicalOr($expr), + $expr instanceof Expr\BinaryOp\LogicalXor => $this->parseBinaryOpLogicalXor($expr), + $expr instanceof Expr\BinaryOp\Minus => $this->parseBinaryOpMinus($expr), + $expr instanceof Expr\Array_ => $this->parseArray($expr), + $expr instanceof Expr\ArrayDimFetch => $this->parseArrayDimFetch($expr), + $expr instanceof Expr\PropertyFetch => $this->parsePropertyFetch($expr), + $expr instanceof Expr\NullsafePropertyFetch => $this->parseNullsafePropertyFetch($expr), + $expr instanceof Expr\NullsafeMethodCall => $this->parseNullsafeMethodCall($expr), + $expr instanceof Expr\BinaryOp\ShiftLeft => $this->parseBinaryOpShiftLeft($expr), + $expr instanceof Expr\BinaryOp\ShiftRight => $this->parseBinaryOpShiftRight($expr), + $expr instanceof Expr\BinaryOp\BitwiseAnd => $this->parseBinaryOpBitwiseAnd($expr), + $expr instanceof Expr\BinaryOp\BitwiseOr => $this->parseBinaryOpBitwiseOr($expr), + $expr instanceof Expr\BinaryOp\BitwiseXor => $this->parseBinaryOpBitwiseXor($expr), + $expr instanceof Expr\BinaryOp\Pipe => $this->parsePipeOperator($expr), + $expr instanceof Expr\BitwiseNot => $this->parseBitwiseNot($expr), + $expr instanceof Expr\BinaryOp\Mod => $this->parseBinaryOpMod($expr), + $expr instanceof Expr\BinaryOp\Pow => $this->parseBinaryOpPow($expr), + $expr instanceof Expr\Ternary => $this->parseTernary($expr), + $expr instanceof Expr\Match_ => $this->parseMatch($expr), + $expr instanceof Expr\FuncCall => $this->parseFuncCall($expr), + $expr instanceof Expr\MethodCall => $this->parseMethodCall($expr), + $expr instanceof Expr\StaticCall => $this->parseStaticCall($expr), + $expr instanceof Expr\StaticPropertyFetch => $this->parseStaticPropertyFetch($expr), + $expr instanceof Expr\ClassConstFetch => $this->parseClassConstFetch($expr), + $expr instanceof Expr\Include_ => $this->parseInclude($expr), + $expr instanceof Expr\Eval_ => $this->parseEval($expr), + $expr instanceof Expr\New_ => $this->parseNew($expr), + $expr instanceof Expr\Clone_ => $this->parseClone($expr), + $expr instanceof Expr\Instanceof_ => $this->parseInstanceof($expr), + $expr instanceof Expr\Throw_ => $this->parseThrow($expr), + $expr instanceof Expr\ShellExec => $this->parseShellExec($expr), + $expr instanceof Expr\Closure => $this->parseClosure($expr), + $expr instanceof Expr\ArrowFunction => $this->parseArrowFunction($expr), + $expr instanceof Node\Name\FullyQualified => $this->parseFullyQualifiedName($expr), + $expr instanceof Node\Scalar\Int_, + $expr instanceof Node\Scalar\Float_, + $expr instanceof Node\Scalar\String_ => $this->parseIdentifier($expr), + $expr instanceof Node\Scalar\MagicConst => $this->parseMagicConst($expr), + $expr instanceof Node\Scalar\InterpolatedString => $this->parseInterpolatedString($expr), + $expr instanceof Expr\Cast\Int_ => $this->parseCastInt($expr), + $expr instanceof Expr\Cast\Double => $this->parseCastDouble($expr), + $expr instanceof Expr\Cast\Bool_ => $this->parseCastBool($expr), + $expr instanceof Expr\Cast\String_ => $this->parseCastString($expr), + $expr instanceof Expr\Cast\Array_ => $this->parseCastArray($expr), + $expr instanceof Expr\Cast\Object_ => $this->parseCastObject($expr), + $expr instanceof Expr\Cast\Void_ => $this->parseCastVoid($expr), + $expr instanceof Expr\ConstFetch => $this->parseConstFetch($expr), + $expr instanceof Expr\UnaryMinus => $this->parseUnaryMinus($expr), + $expr instanceof Expr\UnaryPlus => $this->parseUnaryPlus($expr), + $expr instanceof Node\InterpolatedStringPart => $this->parseInterpolatedStringPart($expr), + $expr instanceof Expr\ErrorSuppress => $this->parseErrorSuppress($expr), + $expr instanceof Expr\Exit_ => $this->parseExit($expr), + $expr instanceof Expr\Yield_ => $this->parseYieldExpr($expr), + $expr instanceof Expr\YieldFrom => $this->parseYieldFromExpr($expr), + default => $this->unsupportedSyntax($expr), + }; } public function stop(string $string): never @@ -1195,12 +1097,12 @@ class CompilerBase implements PropertyAccessContext // evaluated for side effects and then coerced from null. } - protected function isVoidValueExpr(NodeAbstract $expr): bool + protected function isVoidValueExpr(Node $expr): bool { return $this->detectTypeOfExpr($expr) === Type::VOID; } - protected function wrapVoidExprAsNull(NodeAbstract $expr, string $exprCode): string + protected function wrapVoidExprAsNull(Node $expr, string $exprCode): string { if (!$this->isVoidValueExpr($expr)) { return $exprCode; @@ -1209,7 +1111,7 @@ class CompilerBase implements PropertyAccessContext return '((void) (' . $exprCode . '), ' . self::VALUE_NULL . ')'; } - protected function parseExprAsValue(NodeAbstract $expr): string + protected function parseExprAsValue(Node $expr): string { if ($expr instanceof Expr\Cast\Void_) { return $this->parseExpr($expr); @@ -1614,33 +1516,32 @@ class CompilerBase implements PropertyAccessContext return false; } - protected function parseIdentifier(NodeAbstract $expr): string + protected function parseIdentifier(Node $expr): string { - $type = $expr->getType(); - switch ($type) { - case 'Expr_Variable': - return $this->parseVariable($expr); - case 'Name_FullyQualified': - return '\\' . $expr->name; - case 'Name': - case 'VarLikeIdentifier': - case 'Identifier': - return $expr->name; - case 'Scalar_Int': - case 'Scalar_Float': - case 'Scalar_String': - return $this->parseScalar($expr); - case 'Expr_ConstFetch': - return $this->parseConstFetch($expr); - case 'Expr_Assign': - case 'Expr_AssignRef': - if (!$this->isVarExpr($expr->var) && !$this->isPropertyFetch($expr->var) && !$this->isArrayDimFetch($expr->var)) { - $this->fatalError($expr, 'When an assignment expression serves as an rvalue, it must be an assignment of a variable, property, or array element'); - } - return $this->parseExprAsValue($expr); - default: - return $this->parseExprAsValue($expr); + if ($expr instanceof Variable) { + return $this->parseVariable($expr); + } + if ($expr instanceof Node\Name\FullyQualified) { + return '\\' . $expr->toString(); + } + if ($expr instanceof Node\Name || $expr instanceof Node\VarLikeIdentifier || $expr instanceof Node\Identifier) { + return $expr->toString(); + } + if ($expr instanceof Node\Scalar\Int_ + || $expr instanceof Node\Scalar\Float_ + || $expr instanceof Node\Scalar\String_ + ) { + return $this->parseScalar($expr); + } + if ($expr instanceof Expr\ConstFetch) { + return $this->parseConstFetch($expr); + } + if ($expr instanceof Expr\Assign || $expr instanceof Expr\AssignRef) { + if (!$this->isVarExpr($expr->var) && !$this->isPropertyFetch($expr->var) && !$this->isArrayDimFetch($expr->var)) { + $this->fatalError($expr, 'When an assignment expression serves as an rvalue, it must be an assignment of a variable, property, or array element'); + } } + return $this->parseExprAsValue($expr); } protected function parseParamDefaultValue(?NodeAbstract $default): ?string @@ -4634,7 +4535,7 @@ class CompilerBase implements PropertyAccessContext return array_key_exists($name, $this->context->staticVars); } - protected function parseCastDouble(mixed $expr): string + protected function parseCastDouble(Expr\Cast\Double $expr): string { $this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand'); $native = $this->parseNativeObjectExplicitConversion($expr->expr, 'toFloat'); diff --git a/src/Diagnostics/CompilerDiagnosticTrait.php b/src/Diagnostics/CompilerDiagnosticTrait.php index 2f995537..b554995b 100644 --- a/src/Diagnostics/CompilerDiagnosticTrait.php +++ b/src/Diagnostics/CompilerDiagnosticTrait.php @@ -10,7 +10,6 @@ namespace TypePhp\Diagnostics; use PhpParser\Node; use PhpParser\Node\Expr\Variable; -use PhpParser\NodeAbstract; trait CompilerDiagnosticTrait { @@ -22,7 +21,7 @@ trait CompilerDiagnosticTrait $this->getDiagnosticReporter()->fatal($msg); } - public function fatalError(NodeAbstract $node, string $msg): never + public function fatalError(Node $node, string $msg): never { $this->error("{$msg} in {$this->file}:{$node->getStartLine()}"); } @@ -56,12 +55,12 @@ trait CompilerDiagnosticTrait $this->fatalError($node, "The variable `\${$node->name}` is undefined"); } - protected function warningUndefinedBehavior(NodeAbstract $expr): void + protected function warningUndefinedBehavior(Node $expr): void { $this->warning($expr, 'Use this expression carefully, which may be inconsistent with the dynamic execution behavior'); } - protected function dump(NodeAbstract $node): void + protected function dump(Node $node): void { if ($this->debugLine == $node->getStartLine()) { var_dump($node); diff --git a/src/Installer/LinuxPackageManager.php b/src/Installer/LinuxPackageManager.php index a406ec03..fac626a2 100644 --- a/src/Installer/LinuxPackageManager.php +++ b/src/Installer/LinuxPackageManager.php @@ -68,6 +68,7 @@ final readonly class LinuxPackageManager 'apt-get' => ['build-essential', 'cmake', 'pkg-config'], 'dnf' => ['gcc', 'gcc-c++', 'make', 'cmake', 'pkgconf-pkg-config'], 'yum' => ['gcc', 'gcc-c++', 'make', 'cmake', 'pkgconfig'], + default => throw new \LogicException("Unsupported package manager: {$this->command}"), }; } @@ -78,6 +79,7 @@ final readonly class LinuxPackageManager 'apt-get' => ['apt-get', 'install', '-y'], 'dnf' => ['dnf', 'install', '-y'], 'yum' => ['yum', 'install', '-y'], + default => throw new \LogicException("Unsupported package manager: {$this->command}"), }; return [...$prefix, ...$args, ...$packages]; } diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index 843c9c9a..89e34e8c 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -1168,7 +1168,7 @@ trait NativeClassSupportTrait * the reference itself, while assignments, returns, arguments and * comparisons need the typed pointer. */ - protected function normalizeNativeObjectValueExpr(NodeAbstract $expr, string $value): string + protected function normalizeNativeObjectValueExpr(Node $expr, string $value): string { if ($this->classDef?->nativeObject && $this->isVarExpr($expr) diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 6e161de1..33232c7d 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -1151,6 +1151,7 @@ trait AssignOpTrait Type::BIGINT => ['BigInt', ['+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod', '&' => 'bitAnd', '|' => 'bitOr', '^' => 'bitXor', '<<' => 'bitShiftLeft', '>>' => 'bitShiftRight']], Type::DECIMAL => ['Decimal', ['+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod']], Type::BIGFLOAT => ['BigFloat', ['+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div']], + default => $this->fatalError($errorNode, "Unsupported compound assignment type '{$leftType}'"), }; $method = $opMap[$binaryOp] ?? null; @@ -1164,6 +1165,7 @@ trait AssignOpTrait Type::BIGINT => $isShift ? $rightExpr : $this->convertBigIntExpr($rightExpr, $rightType), Type::DECIMAL => $this->convertDecimalExpr($rightExpr, $rightType, $rightNode), Type::BIGFLOAT => $this->convertBigFloatExpr($rightExpr, $rightType), + default => $this->fatalError($errorNode, "Unsupported compound assignment type '{$leftType}'"), }; return 'php::' . $class . '::' . $method . '(' . $leftExpr . ', ' . $convertedRight . ')'; diff --git a/src/Parser/AstNodeType.php b/src/Parser/AstNodeType.php index deda79e4..9a770519 100644 --- a/src/Parser/AstNodeType.php +++ b/src/Parser/AstNodeType.php @@ -11,143 +11,162 @@ namespace TypePhp\Parser; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\VariadicPlaceholder; -use PhpParser\NodeAbstract; trait AstNodeType { - protected function isArrayDimFetch(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\ArrayDimFetch $expr */ + protected function isArrayDimFetch(Node $expr): bool { return $expr instanceof Expr\ArrayDimFetch; } - protected function isVarExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\Variable $expr */ + protected function isVarExpr(Node $expr): bool { return $expr instanceof Expr\Variable; } - protected function isIdExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Identifier $expr */ + protected function isIdExpr(Node $expr): bool { return $expr instanceof Node\Identifier; } - protected function isPropertyFetch(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\PropertyFetch $expr */ + protected function isPropertyFetch(Node $expr): bool { return $expr instanceof Expr\PropertyFetch; } - protected function isStaticPropertyFetch(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\StaticPropertyFetch $expr */ + protected function isStaticPropertyFetch(Node $expr): bool { return $expr instanceof Expr\StaticPropertyFetch; } - protected function isClassConstFetch(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\ClassConstFetch $expr */ + protected function isClassConstFetch(Node $expr): bool { return $expr instanceof Expr\ClassConstFetch; } - protected function isNewExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\New_ $expr */ + protected function isNewExpr(Node $expr): bool { return $expr instanceof Expr\New_; } - protected function isNameExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Name $expr */ + protected function isNameExpr(Node $expr): bool { return $expr instanceof Node\Name; } - protected function isFullNameExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Name\FullyQualified $expr */ + protected function isFullNameExpr(Node $expr): bool { return $expr instanceof Node\Name\FullyQualified; } - protected function isNamedMethod(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Identifier $expr */ + protected function isNamedMethod(Node $expr): bool { return $this->isIdExpr($expr); } - protected function isScalarString(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Scalar\String_ $expr */ + protected function isScalarString(Node $expr): bool { return $expr instanceof Node\Scalar\String_; } - protected function isFuncCallExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\FuncCall $expr */ + protected function isFuncCallExpr(Node $expr): bool { return $expr instanceof Expr\FuncCall; } - protected function isRefvalCall(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\FuncCall $expr */ + protected function isRefvalCall(Node $expr): bool { return $this->isFuncCallExpr($expr) and $this->isNameExpr($expr->name) and $expr->name->toString() === 'refval'; } - protected function isMethodCall(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\MethodCall $expr */ + protected function isMethodCall(Node $expr): bool { return $expr instanceof Expr\MethodCall; } - protected function isStaticCall(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\StaticCall $expr */ + protected function isStaticCall(Node $expr): bool { return $expr instanceof Expr\StaticCall; } - protected function isScalar(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Scalar $expr */ + protected function isScalar(Node $expr): bool { return $expr instanceof Node\Scalar; } - protected function isScalarInt(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Scalar\Int_ $expr */ + protected function isScalarInt(Node $expr): bool { return $expr instanceof Node\Scalar\Int_; } - protected function isScalarBool(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\ConstFetch $expr */ + protected function isScalarBool(Node $expr): bool { return $expr instanceof Expr\ConstFetch and in_array(strtolower($expr->name->toString()), ['true', 'false']); } - protected function getBoolValue(Expr\ConstFetch $expr): string - { - return strcasecmp($expr->name->toString(), 'true') === 0 ? self::VALUE_TRUE : self::VALUE_FALSE; - } - - protected function isMatchExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\Match_ $expr */ + protected function isMatchExpr(Node $expr): bool { return $expr instanceof Expr\Match_; } - protected function isAssignExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\Assign $expr */ + protected function isAssignExpr(Node $expr): bool { return $expr instanceof Expr\Assign; } - protected function isCallExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\FuncCall|Expr\MethodCall|Expr\StaticCall $expr */ + protected function isCallExpr(Node $expr): bool { return $expr instanceof Expr\FuncCall or $expr instanceof Expr\MethodCall or $expr instanceof Expr\StaticCall; } - protected function isPlaceholderExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true VariadicPlaceholder $expr */ + protected function isPlaceholderExpr(Node $expr): bool { return $expr instanceof VariadicPlaceholder; } - protected function isReturnExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Stmt\Return_ $expr */ + protected function isReturnExpr(Node $expr): bool { return $expr instanceof Node\Stmt\Return_; } - protected function isBreakExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Stmt\Break_ $expr */ + protected function isBreakExpr(Node $expr): bool { return $expr instanceof Node\Stmt\Break_; } - protected function isContinueExpr(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Node\Stmt\Continue_ $expr */ + protected function isContinueExpr(Node $expr): bool { return $expr instanceof Node\Stmt\Continue_; } - protected function isThrowExpr(NodeAbstract $expr): bool + protected function isThrowExpr(Node $expr): bool { if ($expr instanceof Node\Stmt\Expression) { $expr = $expr->expr; @@ -155,7 +174,7 @@ trait AstNodeType return $expr instanceof Expr\Throw_; } - protected function isExitExpr(NodeAbstract $expr): bool + protected function isExitExpr(Node $expr): bool { if ($expr instanceof Node\Stmt\Expression) { $expr = $expr->expr; @@ -163,12 +182,14 @@ trait AstNodeType return $expr instanceof Expr\Exit_; } - protected function isEmptyArray(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\Array_ $expr */ + protected function isEmptyArray(Node $expr): bool { return $expr instanceof Expr\Array_ && count($expr->items) === 0; } - protected function isNull(NodeAbstract $expr): bool + /** @phpstan-assert-if-true Expr\ConstFetch $expr */ + protected function isNull(Node $expr): bool { return $expr instanceof Expr\ConstFetch && strcasecmp($expr->name->toString(), 'null') === 0; } diff --git a/src/Parser/StdContainerTrait.php b/src/Parser/StdContainerTrait.php index 00042939..68abab28 100644 --- a/src/Parser/StdContainerTrait.php +++ b/src/Parser/StdContainerTrait.php @@ -811,6 +811,7 @@ trait StdContainerTrait 'toStdVector' => 'vector', 'toStdMap' => 'map', 'toStdOrderedMap' => 'ordered_map', + default => $this->fatalError($expr, "Unknown std conversion method: {$methodName}"), }; if (!$this->isVarExpr($expr->var)) { diff --git a/src/Preprocessor.php b/src/Preprocessor.php index ead80b15..ed3746e0 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -90,7 +90,7 @@ class Preprocessor extends CompilerBase return str_ends_with(strtolower($file), '.php'); } - /** @param array $ast */ + /** @param list $ast */ private function discoverNativeClassDeclarationsInAst(array $ast): void { $finder = new NodeFinder(); @@ -302,7 +302,7 @@ class Preprocessor extends CompilerBase )); $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); - $stmts = $traverser->traverse($ast); + $stmts = $this->requireStatementList($traverser->traverse($ast)); // Keep the resolved declaration AST until convert. Defaults and // constants are validated here, but their C++ expressions are not // generated until the complete symbol table is available. @@ -314,40 +314,24 @@ class Preprocessor extends CompilerBase $this->discoverNativeClassDeclarationsInAst($stmts); foreach ($stmts as $v) { - $type = $v->getType(); - switch ($type) { - case 'Stmt_Namespace': - $this->prepareNamespace($v); - break; - case 'Stmt_Enum': - case 'Stmt_Class': - case 'Stmt_Trait': - $this->prepareClass($v); - break; - case 'Stmt_Interface': - $this->parseInterface($v); - break; - case 'Stmt_Function': - $this->prepareFunction($v); - break; - case 'Stmt_Use': - $this->parseUse($v); - break; - case 'Stmt_GroupUse': - $this->parseGroupUse($v); - break; - case 'Stmt_Declare': - case 'Stmt_Nop': - break; - case 'Stmt_Const': - $this->parseConstDef($v); - break; - case 'Stmt_Expression': - $this->foundStrayCode($v); - break; - default: - $this->fatalError($v, 'Unsupported statement: ' . $type); - break; + if ($v instanceof Node\Stmt\Namespace_) { + $this->prepareNamespace($v); + } elseif ($v instanceof Node\Stmt\Class_ || $v instanceof Node\Stmt\Enum_ || $v instanceof Node\Stmt\Trait_) { + $this->prepareClass($v); + } elseif ($v instanceof Node\Stmt\Interface_) { + $this->parseInterface($v); + } elseif ($v instanceof Node\Stmt\Function_) { + $this->prepareFunction($v); + } elseif ($v instanceof Node\Stmt\Use_) { + $this->parseUse($v); + } elseif ($v instanceof Node\Stmt\GroupUse) { + $this->parseGroupUse($v); + } elseif ($v instanceof Node\Stmt\Const_) { + $this->parseConstDef($v); + } elseif ($v instanceof Node\Stmt\Expression) { + $this->foundStrayCode($v); + } elseif (!$v instanceof Node\Stmt\Declare_ && !$v instanceof Node\Stmt\Nop) { + $this->fatalError($v, 'Unsupported statement: ' . $v->getType()); } } } finally { @@ -355,6 +339,23 @@ class Preprocessor extends CompilerBase } } + /** + * Root parser output must remain a statement list after declaration + * visitors have run. Validate that invariant before storing the AST. + * + * @param list $nodes + * @return list + */ + private function requireStatementList(array $nodes): array + { + foreach ($nodes as $node) { + if (!$node instanceof Node\Stmt) { + throw new \LogicException('Root AST traversal produced a non-statement node: ' . $node->getType()); + } + } + return $nodes; + } + /** * Lower declaration-only constant expressions after every symbol is known. * @@ -735,33 +736,20 @@ class Preprocessor extends CompilerBase $this->namespace = $node->name ? $this->parseIdentifier($node->name) : ''; foreach ($node->stmts as $v2) { - $type2 = $v2->getType(); - switch ($type2) { - case 'Stmt_Class': - case 'Stmt_Enum': - case 'Stmt_Trait': - $this->prepareClass($v2); - break; - case 'Stmt_Function': - $this->prepareFunction($v2); - break; - case 'Stmt_Use': - $this->parseUse($v2); - break; - case 'Stmt_GroupUse': - $this->parseGroupUse($v2); - break; - case 'Stmt_Const': - $this->parseConstDef($v2); - break; - case 'Stmt_Interface': - $this->parseInterface($v2); - break; - case 'Stmt_Nop': - break; - default: - $this->foundStrayCode($v2); - break; + if ($v2 instanceof Node\Stmt\Class_ || $v2 instanceof Node\Stmt\Enum_ || $v2 instanceof Node\Stmt\Trait_) { + $this->prepareClass($v2); + } elseif ($v2 instanceof Node\Stmt\Function_) { + $this->prepareFunction($v2); + } elseif ($v2 instanceof Node\Stmt\Use_) { + $this->parseUse($v2); + } elseif ($v2 instanceof Node\Stmt\GroupUse) { + $this->parseGroupUse($v2); + } elseif ($v2 instanceof Node\Stmt\Const_) { + $this->parseConstDef($v2); + } elseif ($v2 instanceof Node\Stmt\Interface_) { + $this->parseInterface($v2); + } elseif (!$v2 instanceof Node\Stmt\Nop) { + $this->foundStrayCode($v2); } } } diff --git a/src/Translator.php b/src/Translator.php index deb432df..927f5325 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2795,39 +2795,24 @@ CODE; $cppCode = ''; foreach ($stmts as $v) { - $type = $v->getType(); - switch ($type) { - case 'Stmt_Declare': - $this->parseDeclare($v); - break; - case 'Stmt_Namespace': - $cppCode .= $this->parseNamespace($v); - break; - case 'Stmt_Class': - case 'Stmt_Trait': - case 'Stmt_Enum': - $cppCode .= $this->parseClass($v); - break; - case 'Stmt_Use': - $this->parseUse($v); - break; - case 'Stmt_GroupUse': - $this->parseGroupUse($v); - break; - case 'Stmt_Function': - $cppCode .= $this->parseFunction($v) . PHP_EOL; - break; - case 'Stmt_Const': - $this->parseConstDef($v); - break; - case 'Stmt_Interface': - $this->validateInterfaceOverrideAttributes($v); - break; - case 'Stmt_Nop': - break; - default: - $this->unsupportedSyntax($v); - break; + if ($v instanceof Node\Stmt\Declare_) { + $this->parseDeclare($v); + } elseif ($v instanceof Node\Stmt\Namespace_) { + $cppCode .= $this->parseNamespace($v); + } elseif ($v instanceof Node\Stmt\Class_ || $v instanceof Node\Stmt\Trait_ || $v instanceof Node\Stmt\Enum_) { + $cppCode .= $this->parseClass($v); + } elseif ($v instanceof Node\Stmt\Use_) { + $this->parseUse($v); + } elseif ($v instanceof Node\Stmt\GroupUse) { + $this->parseGroupUse($v); + } elseif ($v instanceof Node\Stmt\Function_) { + $cppCode .= $this->parseFunction($v) . PHP_EOL; + } elseif ($v instanceof Node\Stmt\Const_) { + $this->parseConstDef($v); + } elseif ($v instanceof Node\Stmt\Interface_) { + $this->validateInterfaceOverrideAttributes($v); + } elseif (!$v instanceof Node\Stmt\Nop) { + $this->unsupportedSyntax($v); } } @@ -2988,33 +2973,20 @@ CODE; $ns_end = ''; foreach ($node->stmts as $v2) { - $type2 = $v2->getType(); - switch ($type2) { - case 'Stmt_Class': - case 'Stmt_Trait': - case 'Stmt_Enum': - $code .= $this->parseClass($v2); - break; - case 'Stmt_Const': - $this->parseConstDef($v2); - break; - case 'Stmt_Function': - $code .= $this->parseFunction($v2) . PHP_EOL; - break; - case 'Stmt_Use': - $this->parseUse($v2); - break; - case 'Stmt_GroupUse': - $this->parseGroupUse($v2); - break; - case 'Stmt_Interface': - $this->validateInterfaceOverrideAttributes($v2); - break; - case 'Stmt_Nop': - break; - default: - $this->unsupportedSyntax($v2); - break; + if ($v2 instanceof Node\Stmt\Class_ || $v2 instanceof Node\Stmt\Trait_ || $v2 instanceof Node\Stmt\Enum_) { + $code .= $this->parseClass($v2); + } elseif ($v2 instanceof Node\Stmt\Const_) { + $this->parseConstDef($v2); + } elseif ($v2 instanceof Node\Stmt\Function_) { + $code .= $this->parseFunction($v2) . PHP_EOL; + } elseif ($v2 instanceof Node\Stmt\Use_) { + $this->parseUse($v2); + } elseif ($v2 instanceof Node\Stmt\GroupUse) { + $this->parseGroupUse($v2); + } elseif ($v2 instanceof Node\Stmt\Interface_) { + $this->validateInterfaceOverrideAttributes($v2); + } elseif (!$v2 instanceof Node\Stmt\Nop) { + $this->unsupportedSyntax($v2); } } $code .= $ns_end;