diff --git a/phpunit/code/variable-variable-arraydim.php b/phpunit/code/variable-variable-arraydim.php new file mode 100644 index 00000000..9a5db13f --- /dev/null +++ b/phpunit/code/variable-variable-arraydim.php @@ -0,0 +1,8 @@ + 'hello']; + ${$foo['bar']} = 'world'; + echo $hello; +} diff --git a/phpunit/code/variable-variable-function-call.php b/phpunit/code/variable-variable-function-call.php new file mode 100644 index 00000000..26fd0ef1 --- /dev/null +++ b/phpunit/code/variable-variable-function-call.php @@ -0,0 +1,12 @@ +exec('The `$$` syntax is not supported', 'variable-variable-arraydim.php'); + } + + public function testVariableVariableWithFunctionCallThrowsUnsupportedError(): void + { + $this->exec('The `$$` syntax is not supported', 'variable-variable-function-call.php'); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index e92ffb80..e5355646 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1020,6 +1020,12 @@ class CompilerBase implements PropertyAccessContext return ltrim($this->namespace . '\\' . $this->class, '\\'); } + protected function getFullClassLikeName(): string + { + $name = $this->class !== '' ? $this->class : $this->interface; + return ltrim($this->namespace . '\\' . $name, '\\'); + } + protected function getFullMethodName(string $fullClassName, string $method): string { return strtolower($fullClassName . '::' . $method); @@ -1288,7 +1294,7 @@ class CompilerBase implements PropertyAccessContext protected function parseVariable(Variable $expr): string { - if (is_object($expr->name) and $this->isVarExpr($expr->name)) { + if (!is_string($expr->name)) { $this->fatalError($expr, 'The `$$` syntax is not supported'); } if ($this->isSuperGlobal($expr->name)) { diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 16dc4ae3..292967a1 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -104,7 +104,7 @@ trait TypeCheckGenerator } if ($name === 'self') { - $class = $this->getFullClassName(); + $class = $this->getFullClassLikeName(); } elseif ($name === 'parent') { $class = $this->classDef->extends ?? ''; } elseif ($name === 'static') { diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index cf826761..dabef684 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -862,9 +862,17 @@ trait AssignOpTrait protected function parseAssignOpCoalesce(Expr\AssignOp\Coalesce $expr): string { $this->checkLeftValue($expr->var); + + // An undefined variable must exist before generating its isset check. + // Keep it as Variant so NULL remains distinguishable from native defaults. + $var = $this->isVarExpr($expr->var) ? $this->parseIdentifier($expr->var) : null; + if ($var !== null && !$this->hasVar($var)) { + $this->addLocalVar($var, Type::VAR); + } + $isset = $this->parseChainedExpr($expr->var, self::OP_ISSET); - $var = $this->parseWritableIdentifier($expr->var); + $var ??= $this->parseWritableIdentifier($expr->var); $propertyWriteTarget = $this->preparePropertyWriteTarget($expr->var); if ($propertyWriteTarget !== null) { @@ -878,9 +886,6 @@ trait AssignOpTrait if ($this->isVarExpr($expr->expr) and !$this->hasVar($right)) { $this->errorUndefinedVariable($expr->expr); } - if ($this->isVarExpr($expr->var) and !$this->hasVar($var)) { - $this->addLocalVar($var, $this->getNormalAssignType($this->detectTypeOfExpr($expr->expr))); - } return '(' . $isset . '?' . $var . ':(' . $var . ' = ' . $right . '))'; } diff --git a/src/Resolver/NameResolutionTrait.php b/src/Resolver/NameResolutionTrait.php index 307117e5..827db4a3 100644 --- a/src/Resolver/NameResolutionTrait.php +++ b/src/Resolver/NameResolutionTrait.php @@ -166,7 +166,7 @@ trait NameResolutionTrait return $this->getTypeFromZendType($typeNameLower); } else { if ($typeName === 'self') { - $class = $this->getFullClassName(); + $class = $this->getFullClassLikeName(); } elseif ($typeName === 'parent') { if (!$this->classDef) { $this->fatalError($type, 'Cannot use "parent" type declaration outside a class'); diff --git a/src/Translator.php b/src/Translator.php index 676618a3..e7c3838a 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3242,6 +3242,7 @@ CODE; return $arg->typeCheck; } + $declaredClass = $arg->declaredClass ?: $arg->class; return match ($arg->type) { Type::INT => [['kind' => 'isInt']], Type::FLOAT => [['kind' => 'isFloat']], @@ -3249,8 +3250,8 @@ CODE; Type::STR => [['kind' => 'isString']], Type::ARRAY => [['kind' => 'isArray']], Type::RESOURCE => [['kind' => 'isResource']], - Type::OBJECT => $arg->class - ? [['kind' => 'instanceof', 'class' => $arg->class]] + Type::OBJECT => $declaredClass + ? [['kind' => 'instanceof', 'class' => $declaredClass]] : [['kind' => 'isObject']], default => null, }; diff --git a/tests/compiler/class/interface-method-self-return.phpt b/tests/compiler/class/interface-method-self-return.phpt new file mode 100644 index 00000000..90e44eab --- /dev/null +++ b/tests/compiler/class/interface-method-self-return.phpt @@ -0,0 +1,42 @@ +--TEST-- +interface method with `self` return type implemented by class (fluent interface), and namespace block containing comments +--FILE-- +value = $value; + return $this; + } + } + + function main() + { + $test = new TestClass; + // get() returns self, so the result still satisfies the interface + var_dump($test->get() instanceof TestInterface); + var_dump($test === $test->get()); + // fluent chaining of self-returning methods + var_dump($test->get()->setValue(42)->value); + } +} +?> +--EXPECT-- +bool(true) +bool(true) +int(42) diff --git a/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt b/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt new file mode 100644 index 00000000..27dd886b --- /dev/null +++ b/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt @@ -0,0 +1,28 @@ +--TEST-- +assign coalesce on undefined variable +--FILE-- + +--EXPECT-- +int(123) +string(3) "foo" +bool(false) +string(10) "after-null" +default +int(8) diff --git a/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt b/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt new file mode 100644 index 00000000..535212a7 --- /dev/null +++ b/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt @@ -0,0 +1,62 @@ +--TEST-- +Cross-namespace interface implementation parameter compatibility is declaration-order independent +--FILE-- +test(new \B\Impl1())); + var_dump($obj->testParent(new \B\Impl1())); + echo "done\n"; + } +} +?> +--EXPECT-- +bool(true) +bool(true) +done diff --git a/tests/compiler/namespace/interface-self-return-namespaced.phpt b/tests/compiler/namespace/interface-self-return-namespaced.phpt new file mode 100644 index 00000000..4d05a98a --- /dev/null +++ b/tests/compiler/namespace/interface-self-return-namespaced.phpt @@ -0,0 +1,56 @@ +--TEST-- +interface method `self` return type resolves to the interface's fully-qualified name inside a named namespace +--FILE-- +log[] = 'chain'; + return $this; + } + + public function maybe(bool $present): ?self + { + return $present ? $this : null; + } + + public function combine(Chainable $other): self + { + return $this; + } + } +} + +namespace { + function main() + { + $w = new \App\Widget(); + var_dump($w->chain()->chain() instanceof \App\Chainable); + var_dump(count($w->log)); + var_dump($w->maybe(true) instanceof \App\Chainable); + var_dump($w->maybe(false)); + var_dump($w->combine(new \App\Widget()) === $w); + } +} +?> +--EXPECT-- +bool(true) +int(2) +bool(true) +NULL +bool(true)