isNameExpr($expr->class) || !$this->isIdExpr($expr->name)) { return false; } if ($this->resolvePythonModule($expr->class) !== null) { return false; } $class = $this->parseIdentifier($expr->class); if ($class === 'static') { return false; } if ($class === 'self' || $class === 'this_') { if (!$this->classDef) { return false; } $class = '\\' . $this->getFullClassName(); } elseif ($class === 'parent') { if (!$this->classDef || !$this->classDef->extends) { return false; } $class = '\\' . $this->classDef->extends; } $class = $this->getNamespacedClassName($class); $const = $this->parseIdentifier($expr->name); if (strcasecmp($const, 'class') === 0) { return true; } if ($this->hasClass($class)) { if ($this->getClass($class)->enum) { return false; } $nativeConst = $this->findNativeClassConst($expr, $class, $const); return $nativeConst !== false && !$this->classConstantValueRequiresRuntimeCall($nativeConst); } return $this->getInternalScalarClassConstant($class, $const) !== null; } private function classConstantValueRequiresRuntimeCall(string $value): bool { // get_str() is a pure accessor for the module's immutable literal pool, // so moving it into a hoisted local initializer preserves semantics. $value = preg_replace('/\bget_str\(\d+\)/', '', $value); return preg_match('/\b[A-Za-z_][A-Za-z0-9_:]*\s*\(/', $value ?? '') === 1; } /** @return array{mixed}|null */ private function getInternalScalarClassConstant(string $class, string $const): ?array { if (!$this->isInternalClass($class) && !$this->isInternalInterface($class)) { return null; } $reflection = Reflection::getClass($class); $constant = $reflection?->getReflectionConstant($const); if ($constant === false || $constant === null || !$constant->isPublic()) { return null; } $value = $constant->getValue(); return is_scalar($value) ? [$value] : null; } protected function parseClassConstFetch(Expr\ClassConstFetch $expr): string { if (!$this->isNameExpr($expr->class)) { $this->assertNotNativeObjectDynamicClassTarget($expr->class, $expr); } $this->rejectPythonModuleClassConstantFetch($expr); if (!$this->isIdExpr($expr->name)) { return $this->parseDynamicClassConstNameFetch($expr); } if (!$this->isNameExpr($expr->class)) { return $this->parseDynamicClassConstFetch($expr); } $class = $this->parseIdentifier($expr->class); $self = false; if ($class === 'self' or $class === 'this_') { $self = true; // Trait methods are compiled only after their AST is composed into // the consuming class. During trait preprocessing, however, class // constant initializers still belong to the trait itself. Resolve // `self` lexically in both cases; rewriting it to `static` would // incorrectly require a method scope for expressions such as // `const B = [...self::A]`. $class = '\\' . $this->getFullClassName(); } elseif ($class === 'parent') { if (!$this->classDef || !$this->classDef->extends) { $this->fatalError($expr, 'Cannot use "parent" outside a class or class does not extend any class'); } // extends is already fully resolved. Keep the leading slash so the // current namespace is not applied again below. $class = '\\' . $this->classDef->extends; $self = true; } $const = $this->escapeString($this->parseIdentifier($expr->name)); if ($class === 'static') { if ($this->classDef?->nativeObject) { $this->fatalError( $expr, 'Native classes do not support late static binding; use `self::` or a concrete class name', ); } if (!$this->methodDef) { $this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods"); } if ($const === 'class') { return Symbol::getCalledClass(); } else { return Symbol::constant() . '(' . Symbol::getCalledCe() . ', ' . $this->getLiteralString($const) . ')'; } } if ($self or $this->isNameExpr($expr->class)) { $class = $this->getNamespacedClassName($class); } if ($const === 'class') { if ($self or $this->isNameExpr($expr->class)) { return $this->getLiteralString($class); } } if (($self or $this->isNameExpr($expr->class)) and $this->isIdExpr($expr->name)) { if ($this->hasClass($class)) { $classDef = $this->getClass($class); if ($classDef->enum) { $ce = $this->getClassEntryPtr($class); return 'php::getEnumCase(' . $ce . ', ' . $this->getLiteralString($const) . ')'; } $nativeConst = $this->findNativeClassConst($expr, $class, $const); if ($nativeConst) { return $nativeConst; } } $internalConst = $this->getInternalScalarClassConstant($class, $const); if ($internalConst !== null) { return $this->genInternalScalarConstantValue($internalConst[0]); } $ce = $this->getClassEntryPtr($class); return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($const) . ')'; } $name = $class . '::' . $const; $name = $this->getLiteralString($name); return Symbol::constant() . '(' . $name . ')'; } protected function parseDynamicClassConstFetch(Expr\ClassConstFetch $expr): string { $const = $this->escapeString($this->parseIdentifier($expr->name)); // PhpParser represents `$this::CONST` as a dynamic class target even // though the receiver class is the class currently being compiled. // TypePHP class constants are compile-time data, so do not route this // common path through get_class(), string concatenation and ZendVM's // global constant table. if ($this->isVarExpr($expr->class) && $this->parseVariable($expr->class) === 'this_' && $this->classDef && $this->hasClass($this->getFullClassName()) ) { $nativeConst = $this->findNativeClassConst( $expr, $this->getFullClassName(), $const, ); if ($nativeConst !== false) { return $nativeConst; } } $target = $this->materializeDynamicClassConstTarget($expr->class); if ($const === 'class') { return 'php::fn::get_class(' . $target . ')'; } $scope = $this->methodDef && $this->classDef ? $this->getClassEntryPtr($this->getFullClassName()) : 'nullptr'; return 'php::classConstant(' . $target . ', ' . $this->getLiteralString($const) . ', ' . $scope . ')'; } protected function parseDynamicClassConstNameFetch(Expr\ClassConstFetch $expr): string { $scope = $this->methodDef && $this->classDef ? $this->getClassEntryPtr($this->getFullClassName()) : 'nullptr'; if (!$this->isNameExpr($expr->class)) { // PHP evaluates the class target before the dynamic constant name. $target = $this->materializeDynamicClassConstOperand($expr->class, 'class constant target'); $name = $this->materializeDynamicClassConstOperand($expr->name, 'class constant name'); return 'php::classConstant(' . $target . ', ' . $name . ', ' . $scope . ')'; } $class = $this->parseIdentifier($expr->class); if ($class === 'static') { if ($this->classDef?->nativeObject) { $this->fatalError( $expr, 'Native classes do not support late static binding; use `self::` or a concrete class name', ); } if (!$this->methodDef) { $this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods"); } $ce = Symbol::getCalledCe(); } elseif ($class === 'self' or $class === 'this_') { $ce = $this->getClassEntryPtr($this->getFullClassName()); } elseif ($class === 'parent') { if (!$this->classDef || !$this->classDef->extends) { $this->fatalError($expr, 'Cannot use "parent" outside a class or class does not extend any class'); } $ce = $this->getClassEntryPtr($this->classDef->extends); } else { $ce = $this->getClassEntryPtr($this->getNamespacedClassName($class)); } $name = $this->materializeDynamicClassConstOperand($expr->name, 'class constant name'); return 'php::classConstant(' . $ce . ', ' . $name . ', ' . $scope . ')'; } protected function materializeDynamicClassConstTarget(NodeAbstract $expr): string { return $this->materializeDynamicClassConstOperand($expr, 'class constant target'); } protected function materializeDynamicClassConstOperand(NodeAbstract $expr, string $description): string { $this->assertExprCanBeUsedAsValue($expr, $description); [$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); $tmpVar = $this->addTmpVar(Type::VAR); $this->appendCapturedStmtLinesToContext($beforeStmts); $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';'; $this->appendCapturedStmtLinesToContext($afterStmts); return $tmpVar; } }