Skip to content

Commit

Permalink
Compile method calls
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaBehrens committed Dec 4, 2023
1 parent 8abb023 commit 8531626
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 20 deletions.
51 changes: 49 additions & 2 deletions src/Node/Expression/GetAttrExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
use Twig\Extension\SandboxExtension;
use Twig\Template;
use Twig\TypeHint\ArrayType;
use Twig\TypeHint\TypeFactory;
use Twig\TypeHint\ObjectType;

class GetAttrExpression extends AbstractExpression
{
Expand All @@ -35,7 +35,7 @@ public function compile(Compiler $compiler): void
$env = $compiler->getEnvironment();

if ($this->getNode('attribute') instanceof ConstantExpression) {
$type = TypeFactory::createTypeFromText('null');
$type = null;

if ($this->getNode('node')->hasAttribute('typeHint')) {
$type = $this->getNode('node')->getAttribute('typeHint');
Expand All @@ -51,6 +51,53 @@ public function compile(Compiler $compiler): void
;

return;
} else if ($type instanceof ObjectType && $this->getNode('attribute') instanceof ConstantExpression) {
$attributeName = $this->getNode('attribute')->getAttribute('value');

if ($type->getPropertyType($attributeName) !== null) {
$compiler
->raw('((')
->subcompile($this->getNode('node'))
->raw(')?->')
->raw($attributeName)
->raw(')')
;

return;
}

/** Keep similar to @see \Twig\TypeHint\ObjectType::getAttributeType */
$methodNames = [
$attributeName,
'get' . $attributeName,
'is' . $attributeName,
'has' . $attributeName,
];

foreach ($methodNames as $methodName) {
if ($type->getMethodType($methodName) !== null) {
$compiler
->raw('((')
->subcompile($this->getNode('node'))
->raw(')?->')
->raw($methodName)
->raw('(')
;

if ($this->hasNode('arguments') && $this->getNode('arguments') instanceof ArrayExpression && $this->getNode('arguments')->count() > 0) {
for ($argIndex = 0; $argIndex < $this->getNode('arguments')->count(); $argIndex += 2) {
if ($argIndex > 0) {
$compiler->raw(', ');
}

$compiler->subcompile($this->getNode('arguments')->getNode($argIndex + 1));
}
}

$compiler->raw('))');
return;
}
}
}
}

Expand Down
38 changes: 31 additions & 7 deletions src/NodeVisitor/TypeEvaluateNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Twig\Environment;
use Twig\Node\AutoEscapeNode;
use Twig\Node\BodyNode;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Expression\Binary\AddBinary;
Expand Down Expand Up @@ -55,6 +56,7 @@
use Twig\Node\MacroNode;
use Twig\Node\Node;
use Twig\Node\SetNode;
use Twig\Node\TypeHintNode;
use Twig\Node\WithNode;
use Twig\TypeHint\ArrayType;
use Twig\TypeHint\TypeFactory;
Expand All @@ -81,6 +83,14 @@ public function enterNode(Node $node, Environment $env): Node
}
}

if ($node instanceof TypeHintNode) {
$env->getTypeHintStack()->addVariableType($node->getAttribute('name'), TypeFactory::createTypeFromText($node->getAttribute('type')));
}

if ($node instanceof BodyNode) {
$env->getTypeHintStack()->pushMinorStack();
}

return $node;
}

Expand All @@ -89,17 +99,19 @@ public function leaveNode(Node $node, Environment $env): ?Node
$possibleTypes = [];

foreach ($this->getPossibleTypes($node, $env) as $possibleType) {
if (!$possibleType instanceof TypeInterface) {
if (!$possibleType instanceof TypeInterface && $possibleType !== null) {
$possibleType = TypeFactory::createTypeFromText((string) $possibleType);
}

$possibleTypes[] = $possibleType;
}

if (\count($possibleTypes) !== 1) {
$node->setAttribute('typeHint', new UnionType($possibleTypes));
} elseif ($possibleTypes !== []) {
$node->setAttribute('typeHint', $possibleTypes[0]);
if ($possibleTypes !== []) {
if (\count($possibleTypes) === 1) {
$node->setAttribute('typeHint', $possibleTypes[0]);
} else {
$node->setAttribute('typeHint', new UnionType($possibleTypes));
}
}

if ($node instanceof SetNode) {
Expand Down Expand Up @@ -178,6 +190,10 @@ public function leaveNode(Node $node, Environment $env): ?Node
}
}

if ($node instanceof BodyNode) {
$env->getTypeHintStack()->popMinorStack();
}

return $node;
}

Expand Down Expand Up @@ -236,7 +252,11 @@ private function getPossibleTypes(Node $node, Environment $env): iterable
}

if ($node instanceof NameExpression && !$node instanceof AssignNameExpression) {
yield $env->getTypeHintStack()->getVariableType($node->getAttribute('name'));
$result = $env->getTypeHintStack()->getVariableType($node->getAttribute('name'));

if ($result !== null) {
yield $result;
}
}

if ($node instanceof TestExpression) {
Expand Down Expand Up @@ -266,7 +286,11 @@ private function getPossibleTypes(Node $node, Environment $env): iterable
continue;
}

yield $innerNode->getAttribute('typeHint');
$typeHint = $innerNode->getAttribute('typeHint');

if ($typeHint !== null) {
yield $typeHint;
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/TypeHint/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function getAttributeType(string|int $attribute): ?TypeInterface
?? $this->getMethodType('has' . $attribute);
}

private function getPropertyType(string $name): ?TypeInterface
public function getPropertyType(string $name): ?TypeInterface
{
if (\array_key_exists($name, $this->properties)) {
return $this->properties[$name];
Expand Down Expand Up @@ -75,7 +75,7 @@ private function createPropertyType(string $name): ?TypeInterface
}
}

private function getMethodType(string $name): ?TypeInterface
public function getMethodType(string $name): ?TypeInterface
{
if (\array_key_exists($name, $this->methods)) {
return $this->methods[$name];
Expand Down
16 changes: 8 additions & 8 deletions src/TypeHint/TypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ public static function createTypeFromText(string $type): ?TypeInterface
$types = [];

foreach (\explode('|', $type) as $propertyType) {
if (\str_starts_with($propertyType, '\\')) {
try {
$types[] = self::createObjectType($propertyType);
} catch (\Throwable) {
continue;
}
} else {
if ($propertyType === '') {
continue;
}

try {
$types[] = self::createObjectType(\ltrim($propertyType, '\\'));
} catch (\Throwable) {
$types[] = self::createPlainType($propertyType);
}
}
Expand All @@ -62,6 +62,6 @@ private static function createPlainType(string $type): Type
*/
private static function createObjectType(string $class): ObjectType
{
return self::$objectTypeCache[$class] ??= new ObjectType(new \ReflectionClass(\ltrim($class, '\\')));
return self::$objectTypeCache[$class] ??= new ObjectType(new \ReflectionClass($class));
}
}
98 changes: 97 additions & 1 deletion tests/Node/Expression/GetAttrTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,105 @@ public function getTests()
echo ((((($context["foo"] ?? null))["bar"] ?? null))["baz"] ?? null);
PHP,
$optimizedEnv,
true,
];

$tests[] = [
$optimizedEnv->parse($optimizedEnv->tokenize(new Source(<<<'TWIG'
{% type obj "\\Twig\\Tests\\Node\\Expression\\ClassWithPublicProperty" %}
{{ obj.name|raw }}
TWIG, 'index.twig')))->getNode('body'),
<<<'PHP'
// line 1
// line 2
echo ((($context["obj"] ?? null))?->name);
PHP,
$optimizedEnv,
];

$tests[] = [
$optimizedEnv->parse($optimizedEnv->tokenize(new Source(<<<'TWIG'
{% type obj "\\Twig\\Tests\\Node\\Expression\\ClassWithPublicGetter" %}
{{ obj.name|raw }}
TWIG, 'index.twig')))->getNode('body'),
<<<'PHP'
// line 1
// line 2
echo ((($context["obj"] ?? null))?->getname());
PHP,
$optimizedEnv,
];

$tests[] = [
$optimizedEnv->parse($optimizedEnv->tokenize(new Source(<<<'TWIG'
{% type obj "\\Twig\\Tests\\Node\\Expression\\ClassWithPublicFactory" %}
{{ obj.byName("foobar")|raw }}
TWIG, 'index.twig')))->getNode('body'),
<<<'PHP'
// line 1
// line 2
echo ((($context["obj"] ?? null))?->byName("foobar"));
PHP,
$optimizedEnv,
];

$tests[] = [
$optimizedEnv->parse($optimizedEnv->tokenize(new Source(<<<'TWIG'
{% type obj "\\Twig\\Tests\\Node\\Expression\\ClassWithPublicComplexGetter" %}
{{ obj.instance.name|raw }}
TWIG, 'index.twig')))->getNode('body'),
<<<'PHP'
// line 1
// line 2
echo ((((($context["obj"] ?? null))?->getinstance()))?->getname());
PHP,
$optimizedEnv,
];

return $tests;
}
}

class ClassWithPublicProperty
{
public function __construct(
public string $name
)
{
}
}

class ClassWithPublicGetter
{
public function __construct(
private string $name
)
{
}

public function getName(): string
{
return $this->name;
}
}

class ClassWithPublicFactory
{
public function byName(string $name): string
{
return $name;
}
}

class ClassWithPublicComplexGetter
{
public function __construct(
private string $name
)
{
}

public function getInstance(): ClassWithPublicGetter
{
return new ClassWithPublicGetter($this->name);
}
}

0 comments on commit 8531626

Please sign in to comment.