Skip to content

Commit

Permalink
Allow union type predicted type access
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaBehrens committed Dec 5, 2023
1 parent 8531626 commit 6fa36e8
Show file tree
Hide file tree
Showing 2 changed files with 292 additions and 80 deletions.
344 changes: 270 additions & 74 deletions src/Node/Expression/GetAttrExpression.php
Expand Up @@ -13,10 +13,13 @@
namespace Twig\Node\Expression;

use Twig\Compiler;
use Twig\Environment;
use Twig\Extension\SandboxExtension;
use Twig\Template;
use Twig\TypeHint\ArrayType;
use Twig\TypeHint\ObjectType;
use Twig\TypeHint\TypeInterface;
use Twig\TypeHint\UnionType;

class GetAttrExpression extends AbstractExpression
{
Expand All @@ -41,63 +44,23 @@ public function compile(Compiler $compiler): void
$type = $this->getNode('node')->getAttribute('typeHint');
}

if ($type instanceof ArrayType) {
$compiler
->raw('((')
->subcompile($this->getNode('node'))
->raw(')[')
->subcompile($this->getNode('attribute'))
->raw('] ?? null)')
;
if ($type instanceof TypeInterface) {
$sourceCompiler = $this->createNodeSourceCompiler();
$accessCompiler = $this->createAccessCompiler($type, $env);

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;
if (true || $accessCompiler['condition'] === null) {
$accessCompiler['accessor']($compiler, $sourceCompiler);
} else {
$compiler->raw('(');
$accessCompiler['condition']($compiler, $sourceCompiler);
$compiler->raw(' ? ');
$accessCompiler['accessor']($compiler, $sourceCompiler);
$compiler->raw(' : ');
$this->createGuessingAccessCompiler($env->hasExtension(SandboxExtension::class))['accessor']($compiler, $sourceCompiler);
$compiler->raw(')');
}

/** 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;
}
}
return;
}
}

Expand Down Expand Up @@ -126,31 +89,264 @@ public function compile(Compiler $compiler): void
return;
}

$compiler->raw('twig_get_attribute($this->env, $this->source, ');
$this->createGuessingAccessCompiler($env->hasExtension(SandboxExtension::class))['accessor']($compiler, $this->createNodeSourceCompiler());
}

/**
* @return array{
* condition: \Closure(Compiler, \Closure(Compiler): void): void|null,
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
* }
*/
private function createAccessCompiler(TypeInterface $type, Environment $env): array
{
if ($type instanceof UnionType) {
return $this->createUnionAccessCompiler($type, $env);
}

if ($type instanceof ArrayType) {
return $this->createArrayAccessCompiler();
}

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

if ($type->getPropertyType($attributeName) !== null) {
return $this->createObjectPropertyAccessCompiler($type, $attributeName);
}

/** 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) {
continue;
}

if ($this->getAttribute('ignore_strict_check')) {
$this->getNode('node')->setAttribute('ignore_strict_check', true);
return $this->createObjectMethodAccessCompiler($type, $methodName);
}
}

$compiler
->subcompile($this->getNode('node'))
->raw(', ')
->subcompile($this->getNode('attribute'))
;
return $this->createGuessingAccessCompiler($env->hasExtension(SandboxExtension::class));
}

/**
* @return array{
* condition: null,
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
* }
*/
private function createGuessingAccessCompiler(bool $isSandboxed): array
{
return [
'condition' => null,
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler) use ($isSandboxed): void {
$compiler->raw('twig_get_attribute($this->env, $this->source, ');

if ($this->getAttribute('ignore_strict_check')) {
$this->getNode('node')->setAttribute('ignore_strict_check', true);
}

$sourceCompiler($compiler);

$compiler
->raw(', ')
->subcompile($this->getNode('attribute'))
;

if ($this->hasNode('arguments')) {
$compiler->raw(', ')->subcompile($this->getNode('arguments'));
} else {
$compiler->raw(', []');
}

$compiler->raw(', ')
->repr($this->getAttribute('type'))
->raw(', ')->repr($this->getAttribute('is_defined_test'))
->raw(', ')->repr($this->getAttribute('ignore_strict_check'))
->raw(', ')->repr($isSandboxed)
->raw(', ')->repr($this->getNode('node')->getTemplateLine())
->raw(')')
;
},
];
}

/**
* @return array{
* condition: \Closure(Compiler, \Closure(Compiler): void): void,
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
* }
*/
private function createObjectMethodAccessCompiler(ObjectType $type, string $attributeName): array
{
return [
'condition' => function (Compiler $compiler, \Closure $sourceCompiler) use ($type): void {
$sourceCompiler($compiler);
$compiler->raw(' instanceof \\')->raw($type->getType());
},
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler) use ($attributeName): void {
$compiler->raw('(');

$sourceCompiler($compiler);

if ($this->hasNode('arguments')) {
$compiler->raw(', ')->subcompile($this->getNode('arguments'));
} else {
$compiler->raw(', []');
$compiler->raw('?->')->raw($attributeName)->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 array{
* condition: \Closure(Compiler, \Closure(Compiler): void): void,
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
* }
*/
private function createObjectPropertyAccessCompiler(ObjectType $type, string $attributeName): array
{
return [
'condition' => function (Compiler $compiler, \Closure $sourceCompiler) use ($type): void {
$sourceCompiler($compiler);
$compiler->raw(' instanceof \\')->raw($type->getType());
},
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler) use ($attributeName): void {
$sourceCompiler($compiler);
$compiler
->raw('?->')
->raw($attributeName);
},
];
}

/**
* @return array{
* condition: null,
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
* }
*/
private function createUnionAccessCompiler(UnionType $type, Environment $env): array
{
$accessors = [];

foreach ($type->getTypes() as $innerType) {
$accessors[] = $this->createAccessCompiler($innerType, $env);
}

$compiler->raw(', ')
->repr($this->getAttribute('type'))
->raw(', ')->repr($this->getAttribute('is_defined_test'))
->raw(', ')->repr($this->getAttribute('ignore_strict_check'))
->raw(', ')->repr($env->hasExtension(SandboxExtension::class))
->raw(', ')->repr($this->getNode('node')->getTemplateLine())
->raw(')')
;
return [
'condition' => null,
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler) use ($accessors) {
$compiler->raw('match ([');
$compiler->indent();
$sourceCompiler($compiler);
$compiler->raw(", true][1]) {\n");

foreach ($accessors as $accessor) {
if ($accessor['condition'] === null) {
$compiler->raw('default');
} else {
$accessor['condition']($compiler, $sourceCompiler);
}

$compiler->raw(' => ');
$accessor['accessor']($compiler, $sourceCompiler);
$compiler->raw(";\n");
}

$compiler->outdent();
$compiler->raw('}');
}
];
}

/**
* @return array{
* condition: \Closure(Compiler, \Closure(Compiler): void): void,
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
* }
*/
private function createArrayAccessCompiler(): array
{
return [
'condition' => function (Compiler $compiler, \Closure $sourceCompiler): void {
$compiler->raw('(\is_array(');
$sourceCompiler($compiler);
$compiler->raw(') || ');
$sourceCompiler($compiler);
$compiler->raw(' instanceof \\ArrayAccess)');
},
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler): void {
$compiler->raw('(');
$sourceCompiler($compiler);
$compiler
->raw('[')
->subcompile($this->getNode('attribute'))
->raw('] ?? null)');
},
];
}

/**
* @return \Closure(Compiler): void
*/
private function createAutoInlineSourceCompiler(): \Closure
{
$varName = null;
$sourceCompiler = $this->createNodeSourceCompiler();

return function (Compiler $compiler) use (&$varName, &$sourceCompiler): void {
if ($varName === null) {
$varName = $compiler->getVarName();
$newSourceCompiler = $this->createVarNameSourceCompiler($varName);

$compiler->raw('(');
$newSourceCompiler($compiler);
$compiler->raw(' = ');
$sourceCompiler($compiler);
$compiler->raw(')');

$sourceCompiler = $newSourceCompiler;
} else {
$sourceCompiler($compiler);
}
};
}

/**
* @return \Closure(Compiler): void
*/
private function createNodeSourceCompiler(): \Closure
{
return function (Compiler $compiler): void {
$compiler->subcompile($this->getNode('node'));
};
}

/**
* @return \Closure(Compiler): void
*/
private function createVarNameSourceCompiler(string $varName): \Closure
{
return function (Compiler $compiler) use ($varName): void {
$compiler
->raw('$')
->raw($varName)
;
};
}
}

0 comments on commit 6fa36e8

Please sign in to comment.