diff --git a/doc/tags/for.rst b/doc/tags/for.rst index 549189b20..fdc41d678 100644 --- a/doc/tags/for.rst +++ b/doc/tags/for.rst @@ -45,7 +45,7 @@ The ``..`` operator can take any expression at both sides: * {{ letter }} {% endfor %} -.. tip: +.. tip:: If you need a step different from 1, you can use the ``range`` function instead. @@ -94,6 +94,21 @@ the :doc:`slice <../filters/slice>` filter: {% endfor %} +Iterating over a String +----------------------- + +To iterate over the characters of a string, use the +:doc:`split <../filters/split>` filter: + +.. code-block:: html+twig + +

Characters

+ + Adding a Condition ------------------ diff --git a/doc/tags/index.rst b/doc/tags/index.rst index b3c104080..692ca6094 100644 --- a/doc/tags/index.rst +++ b/doc/tags/index.rst @@ -21,6 +21,7 @@ Tags macro sandbox set + types use verbatim with diff --git a/doc/tags/types.rst b/doc/tags/types.rst new file mode 100644 index 000000000..389afc1fc --- /dev/null +++ b/doc/tags/types.rst @@ -0,0 +1,42 @@ +``types`` +========= + +.. versionadded:: 3.13 + + The ``types`` tag was added in Twig 3.13. This tag is **experimental** and can change based on usage and feedback. + +The ``types`` tag declares the types of template variables. + +To do this, specify a :ref:`mapping ` of names to their types as strings. + +Here is how to declare that ``foo`` is a boolean, while ``bar`` is an integer (see note below): + +.. code-block:: twig + + {% types { + foo: 'bool', + bar: 'int', + } %} + +You can declare variables as optional by adding the ``?`` suffix: + +.. code-block:: twig + + {% types { + foo: 'bool', + bar?: 'int', + } %} + +By default, this tag does not affect the template compilation or runtime behavior. + +Its purpose is to enable designers and developers to document and specify the context's available +and/or required variables. While Twig itself does not validate variables or their types, this tag enables extensions +to do this. + +Additionally, :ref:`Twig extensions ` can analyze these tags to perform compile-time and +runtime analysis of templates. + +.. note:: + + The syntax for and contents of type strings are intentionally left out of scope. + diff --git a/src/Compiler.php b/src/Compiler.php index 6aeb1d752..8310e4f6d 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -22,17 +22,16 @@ class Compiler private ?int $lastLine; private string $source; private int $indentation; - private Environment $env; - private array $debugInfo; + private array $debugInfo = []; private int $sourceOffset; private int $sourceLine; - private int $varNameSalt; + private int $varNameSalt = 0; private string|false $didUseEcho = false; private array $didUseEchoStack = []; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + private Environment $env, + ) { $this->reset(); } diff --git a/src/Environment.php b/src/Environment.php index d6c6c81cb..5441157a5 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -112,7 +112,7 @@ class Environment * (default to -1 which means that all optimizations are enabled; * set it to 0 to disable). */ - public function __construct(LoaderInterface $loader, $options = []) + public function __construct(LoaderInterface $loader, array $options = []) { $this->setLoader($loader); diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index fbedcc547..c116aab72 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -47,17 +47,15 @@ class ExpressionParser public const OPERATOR_LEFT = 1; public const OPERATOR_RIGHT = 2; - private Parser $parser; - private Environment $env; /** @var array}> */ private array $unaryOperators; /** @var array, associativity: self::OPERATOR_*}> */ private array $binaryOperators; - public function __construct(Parser $parser, Environment $env) - { - $this->parser = $parser; - $this->env = $env; + public function __construct( + private Parser $parser, + private Environment $env, + ) { $this->unaryOperators = $env->getUnaryOperators(); $this->binaryOperators = $env->getBinaryOperators(); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 111a37fd4..f9996abc7 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -86,6 +86,7 @@ use Twig\TokenParser\ImportTokenParser; use Twig\TokenParser\IncludeTokenParser; use Twig\TokenParser\MacroTokenParser; use Twig\TokenParser\SetTokenParser; +use Twig\TokenParser\TypesTokenParser; use Twig\TokenParser\UseTokenParser; use Twig\TokenParser\WithTokenParser; use Twig\TwigFilter; @@ -186,6 +187,7 @@ final class CoreExtension extends AbstractExtension new ImportTokenParser(), new FromTokenParser(), new SetTokenParser(), + new TypesTokenParser(), new FlushTokenParser(), new DoTokenParser(), new EmbedTokenParser(), @@ -1259,6 +1261,12 @@ final class CoreExtension extends AbstractExtension } /** + * @template TSequence + * + * @param TSequence $seq + * + * @return ($seq is iterable ? TSequence : array{}) + * * @internal */ public static function toArray($seq, $preserveKeys = true) diff --git a/src/Extension/OptimizerExtension.php b/src/Extension/OptimizerExtension.php index bf2cd7bf7..d3fe46a67 100644 --- a/src/Extension/OptimizerExtension.php +++ b/src/Extension/OptimizerExtension.php @@ -15,11 +15,9 @@ use Twig\NodeVisitor\OptimizerNodeVisitor; final class OptimizerExtension extends AbstractExtension { - private int $optimizers; - - public function __construct(int $optimizers = -1) - { - $this->optimizers = $optimizers; + public function __construct( + private int $optimizers = -1, + ) { } public function getNodeVisitors(): array diff --git a/src/Loader/ArrayLoader.php b/src/Loader/ArrayLoader.php index a7cb64d1c..b4127e2d0 100644 --- a/src/Loader/ArrayLoader.php +++ b/src/Loader/ArrayLoader.php @@ -28,17 +28,12 @@ use Twig\Source; */ final class ArrayLoader implements LoaderInterface { - /** - * @var array - */ - private array $templates = []; - /** * @param array $templates An array of templates (keys are the names, and values are the source code) */ - public function __construct(array $templates = []) - { - $this->templates = $templates; + public function __construct( + private array $templates = [], + ) { } public function setTemplate(string $name, string $template): void diff --git a/src/Loader/ChainLoader.php b/src/Loader/ChainLoader.php index 1f0a6a393..deca5d50a 100644 --- a/src/Loader/ChainLoader.php +++ b/src/Loader/ChainLoader.php @@ -21,11 +21,6 @@ use Twig\Source; */ final class ChainLoader implements LoaderInterface { - /** - * @var \Traversable|LoaderInterface[] - */ - private iterable $loaders = []; - /** * @var array */ @@ -34,9 +29,9 @@ final class ChainLoader implements LoaderInterface /** * @param iterable $loaders */ - public function __construct(iterable $loaders = []) - { - $this->loaders = $loaders; + public function __construct( + private iterable $loaders = [], + ) { } public function addLoader(LoaderInterface $loader): void diff --git a/src/Markup.php b/src/Markup.php index 81f5ae7a5..950c68a29 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -19,7 +19,7 @@ namespace Twig; class Markup implements \Countable, \JsonSerializable, \Stringable { private string $content; - private string $charset; + private ?string $charset; public function __construct($content, $charset) { diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index c8c15da2f..856c2547c 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -108,7 +108,7 @@ final class ModuleNode extends Node $parent = $this->getNode('parent'); $compiler - ->write("protected function doGetParent(array \$context)\n", "{\n") + ->write("protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper\n", "{\n") ->indent() ->addDebugInfo($parent) ->write('return ') @@ -153,7 +153,9 @@ final class ModuleNode extends Node ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") ->write("use Twig\Source;\n") - ->write("use Twig\Template;\n\n") + ->write("use Twig\Template;\n") + ->write("use Twig\TemplateWrapper;\n") + ->write("\n") ; } $compiler @@ -164,6 +166,9 @@ final class ModuleNode extends Node ->write("{\n") ->indent() ->write("private Source \$source;\n") + ->write("/**\n") + ->write(" * @var array\n") + ->write(" */\n") ->write("private array \$macros = [];\n\n") ; } @@ -370,7 +375,7 @@ final class ModuleNode extends Node ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function getTemplateName()\n", "{\n") + ->write("public function getTemplateName(): string\n", "{\n") ->indent() ->write('return ') ->repr($this->getSourceContext()->getName()) @@ -427,7 +432,7 @@ final class ModuleNode extends Node ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function isTraitable()\n", "{\n") + ->write("public function isTraitable(): bool\n", "{\n") ->indent() ->write("return false;\n") ->outdent() @@ -441,7 +446,7 @@ final class ModuleNode extends Node ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function getDebugInfo()\n", "{\n") + ->write("public function getDebugInfo(): array\n", "{\n") ->indent() ->write(\sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) ->outdent() @@ -452,7 +457,7 @@ final class ModuleNode extends Node protected function compileGetSourceContext(Compiler $compiler) { $compiler - ->write("public function getSourceContext()\n", "{\n") + ->write("public function getSourceContext(): Source\n", "{\n") ->indent() ->write('return new Source(') ->string($compiler->getEnvironment()->isDebug() ? $this->getSourceContext()->getCode() : '') diff --git a/src/Node/TypesNode.php b/src/Node/TypesNode.php new file mode 100644 index 000000000..c4e0f081b --- /dev/null +++ b/src/Node/TypesNode.php @@ -0,0 +1,28 @@ + + */ +#[YieldReady] +class TypesNode extends Node +{ + /** + * @param array $types + */ + public function __construct(array $types, int $lineno) + { + parent::__construct([], ['mapping' => $types], $lineno); + } + + public function compile(Compiler $compiler): void + { + // Don't compile anything. + } +} diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 54bbf7cd1..2fb743ce1 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -51,18 +51,16 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface * @var string[] */ private array $loopsTargets = []; - private int $optimizers; /** * @param int $optimizers The optimizer mode */ - public function __construct(int $optimizers = -1) - { + public function __construct( + private int $optimizers = -1, + ) { if ($optimizers > (self::OPTIMIZE_FOR)) { throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); } - - $this->optimizers = $optimizers; } public function enterNode(Node $node, Environment $env): Node diff --git a/src/Parser.php b/src/Parser.php index 4ce372afd..5834d834c 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -53,7 +53,6 @@ class Parser * @var MacroNode[] */ private array $macros; - private Environment $env; private array $importedSymbols; /** * @var Node[] @@ -65,9 +64,9 @@ class Parser private array $embeddedTemplates = []; private int $varNameSalt = 0; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + private Environment $env, + ) { } public function getVarName(): string diff --git a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php index 1b9d6f8e0..0a2a87f81 100644 --- a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php +++ b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php @@ -27,13 +27,12 @@ use Twig\Profiler\Profile; */ final class ProfilerNodeVisitor implements NodeVisitorInterface { - private string $extensionName; private string $varName; - public function __construct(string $extensionName) - { - $this->extensionName = $extensionName; - $this->varName = \sprintf('__internal_%s', hash('xxh128', $extensionName)); + public function __construct( + private string $extensionName, + ) { + $this->varName = \sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); } public function enterNode(Node $node, Environment $env): Node diff --git a/src/Profiler/Profile.php b/src/Profiler/Profile.php index 042baaff6..57204f8d5 100644 --- a/src/Profiler/Profile.php +++ b/src/Profiler/Profile.php @@ -21,9 +21,6 @@ final class Profile implements \IteratorAggregate, \Serializable public const TEMPLATE = 'template'; public const MACRO = 'macro'; - private string $template; - private string $name; - private string $type; private array $starts = []; private array $ends = []; /** @@ -31,10 +28,11 @@ final class Profile implements \IteratorAggregate, \Serializable */ private array $profiles = []; - public function __construct(string $template = 'main', string $type = self::ROOT, string $name = 'main') - { - $this->template = $template; - $this->type = $type; + public function __construct( + private string $template = 'main', + private string $type = self::ROOT, + private string $name = 'main', + ) { $this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name; $this->enter(); } diff --git a/src/RuntimeLoader/ContainerRuntimeLoader.php b/src/RuntimeLoader/ContainerRuntimeLoader.php index 6646cfbaa..89862b09f 100644 --- a/src/RuntimeLoader/ContainerRuntimeLoader.php +++ b/src/RuntimeLoader/ContainerRuntimeLoader.php @@ -23,11 +23,9 @@ use Psr\Container\ContainerInterface; */ class ContainerRuntimeLoader implements RuntimeLoaderInterface { - private ContainerInterface $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; + public function __construct( + private ContainerInterface $container, + ) { } public function load(string $class): ?object diff --git a/src/RuntimeLoader/FactoryRuntimeLoader.php b/src/RuntimeLoader/FactoryRuntimeLoader.php index 5adfa395d..71950cfd2 100644 --- a/src/RuntimeLoader/FactoryRuntimeLoader.php +++ b/src/RuntimeLoader/FactoryRuntimeLoader.php @@ -18,17 +18,12 @@ namespace Twig\RuntimeLoader; */ class FactoryRuntimeLoader implements RuntimeLoaderInterface { - /** - * @var array - */ - private array $map; - /** * @param array $map An array where keys are class names and values factory callables */ - public function __construct(array $map = []) - { - $this->map = $map; + public function __construct( + private array $map = [], + ) { } public function load(string $class): ?object diff --git a/src/Source.php b/src/Source.php index 87427da94..0f626b62d 100644 --- a/src/Source.php +++ b/src/Source.php @@ -18,20 +18,16 @@ namespace Twig; */ final class Source { - private string $code; - private string $name; - private string $path; - /** * @param string $code The template source code * @param string $name The template logical name * @param string $path The filesystem path of the template if any */ - public function __construct(string $code, string $name, string $path = '') - { - $this->code = $code; - $this->name = $name; - $this->path = $path; + public function __construct( + private string $code, + private string $name, + private string $path = '', + ) { } public function getCode(): string diff --git a/src/Template.php b/src/Template.php index d85c9fcf5..fa7c0065c 100644 --- a/src/Template.php +++ b/src/Template.php @@ -34,38 +34,33 @@ abstract class Template protected $parent; protected $parents = []; - protected $env; protected $blocks = []; protected $traits = []; protected $extensions = []; protected $sandbox; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + protected Environment $env, + ) { $this->extensions = $env->getExtensions(); } /** * Returns the template name. - * - * @return string The template name */ - abstract public function getTemplateName(); + abstract public function getTemplateName(): string; /** * Returns debug information about the template. * - * @return array Debug information + * @return array Debug information */ - abstract public function getDebugInfo(); + abstract public function getDebugInfo(): array; /** * Returns information about the original template source code. - * - * @return Source */ - abstract public function getSourceContext(); + abstract public function getSourceContext(): Source; /** * Auto-generated method to display the template with the given context. @@ -111,12 +106,12 @@ abstract class Template return $this->parents[$parent]; } - protected function doGetParent(array $context) + protected function doGetParent(array $context): bool|string|self|TemplateWrapper { return false; } - public function isTraitable() + public function isTraitable(): bool { return true; } diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index 45e1b4b93..c31f50161 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -18,19 +18,16 @@ namespace Twig; */ final class TemplateWrapper { - private Environment $env; - private Template $template; - /** * This method is for internal use only and should never be called * directly (use Twig\Environment::load() instead). * * @internal */ - public function __construct(Environment $env, Template $template) - { - $this->env = $env; - $this->template = $template; + public function __construct( + private Environment $env, + private Template $template, + ) { } public function render(array $context = []): string diff --git a/src/Token.php b/src/Token.php index fe1738a99..6bbee7841 100644 --- a/src/Token.php +++ b/src/Token.php @@ -17,10 +17,6 @@ namespace Twig; */ final class Token { - private $value; - private int $type; - private int $lineno; - public const EOF_TYPE = -1; public const TEXT_TYPE = 0; public const BLOCK_START_TYPE = 1; @@ -37,11 +33,11 @@ final class Token public const ARROW_TYPE = 12; public const SPREAD_TYPE = 13; - public function __construct(int $type, $value, int $lineno) - { - $this->type = $type; - $this->value = $value; - $this->lineno = $lineno; + public function __construct( + private int $type, + private $value, + private int $lineno, + ) { } public function __toString() diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php new file mode 100644 index 000000000..b579fd0c0 --- /dev/null +++ b/src/TokenParser/TypesTokenParser.php @@ -0,0 +1,85 @@ + + * @internal + */ +final class TypesTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $stream = $this->parser->getStream(); + + $types = $this->parseSimpleMappingExpression($stream); + + $stream->expect(Token::BLOCK_END_TYPE); + + return new TypesNode($types, $token->getLine()); + } + + /** + * @return array + * + * @throws SyntaxError + */ + private function parseSimpleMappingExpression(TokenStream $stream): array + { + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); + + $types = []; + + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + $nameToken = $stream->expect(Token::NAME_TYPE); + $isOptional = $stream->nextIf(Token::PUNCTUATION_TYPE, '?') !== null; + + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); + + $valueToken = $stream->expect(Token::STRING_TYPE); + + $types[$nameToken->getValue()] = [ + 'type' => $valueToken->getValue(), + 'optional' => $isOptional, + ]; + } + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + + return $types; + } + + public function getTag(): string + { + return 'types'; + } +} diff --git a/src/TokenStream.php b/src/TokenStream.php index 5dc234f69..b99efd04f 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -21,16 +21,15 @@ use Twig\Error\SyntaxError; */ final class TokenStream { - /** - * @var Token[] - */ - private array $tokens; private int $current = 0; - private Source $source; - public function __construct(array $tokens, ?Source $source = null) - { - $this->tokens = $tokens; + /** + * @param Token[] $tokens + */ + public function __construct( + private array $tokens, + private ?Source $source = null, + ) { $this->source = $source ?: new Source('', ''); } diff --git a/src/Util/DeprecationCollector.php b/src/Util/DeprecationCollector.php index af882257d..0ea26ed4b 100644 --- a/src/Util/DeprecationCollector.php +++ b/src/Util/DeprecationCollector.php @@ -20,11 +20,9 @@ use Twig\Source; */ final class DeprecationCollector { - private Environment $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } /** diff --git a/tests/Fixtures/tags/for/for_on_strings.test b/tests/Fixtures/tags/for/for_on_strings.test new file mode 100644 index 000000000..62d484269 --- /dev/null +++ b/tests/Fixtures/tags/for/for_on_strings.test @@ -0,0 +1,13 @@ +--TEST-- +"for" tag can iterate over a string via the "split" filter +--TEMPLATE-- +{% set jp = "諺 / ことわざ" %} + +{% for letter in jp|split('') -%} + -{{- letter }} + {{- loop.last ? '.' }} +{%- endfor %} +--DATA-- +return [] +--EXPECT-- +-諺- -/- -こ-と-わ-ざ. diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index eef62f8f5..f3081dff9 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -23,6 +23,8 @@ use Twig\Node\Node; use Twig\Node\SetNode; use Twig\Node\TextNode; use Twig\Source; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\Test\NodeTestCase; class ModuleTest extends NodeTestCase @@ -73,11 +75,15 @@ use Twig\Sandbox\SecurityNotAllowedFilterError; use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; /* foo.twig */ class __TwigTemplate_%x extends Template { private Source \$source; + /** + * @var array + */ private array \$macros = []; public function __construct(Environment \$env) @@ -103,7 +109,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getTemplateName() + public function getTemplateName(): string { return "foo.twig"; } @@ -111,12 +117,12 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getDebugInfo() + public function getDebugInfo(): array { - return array ( 38 => 1,); + return array ( 42 => 1,); } - public function getSourceContext() + public function getSourceContext(): Source { return new Source("", "foo.twig", ""); } @@ -145,11 +151,15 @@ use Twig\Sandbox\SecurityNotAllowedFilterError; use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; /* foo.twig */ class __TwigTemplate_%x extends Template { private Source \$source; + /** + * @var array + */ private array \$macros = []; public function __construct(Environment \$env) @@ -162,7 +172,7 @@ class __TwigTemplate_%x extends Template ]; } - protected function doGetParent(array \$context) + protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper { // line 1 return "layout.twig"; @@ -181,7 +191,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getTemplateName() + public function getTemplateName(): string { return "foo.twig"; } @@ -189,7 +199,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function isTraitable() + public function isTraitable(): bool { return false; } @@ -197,12 +207,12 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getDebugInfo() + public function getDebugInfo(): array { - return array ( 44 => 1, 42 => 2, 35 => 1,); + return array ( 48 => 1, 46 => 2, 39 => 1,); } - public function getSourceContext() + public function getSourceContext(): Source { return new Source("", "foo.twig", ""); } @@ -236,11 +246,15 @@ use Twig\Sandbox\SecurityNotAllowedFilterError; use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; /* foo.twig */ class __TwigTemplate_%x extends Template { private Source \$source; + /** + * @var array + */ private array \$macros = []; public function __construct(Environment \$env) @@ -253,7 +267,7 @@ class __TwigTemplate_%x extends Template ]; } - protected function doGetParent(array \$context) + protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper { // line 2 return \$this->loadTemplate(((true) ? ("foo") : ("foo")), "foo.twig", 2); @@ -271,7 +285,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getTemplateName() + public function getTemplateName(): string { return "foo.twig"; } @@ -279,7 +293,7 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function isTraitable() + public function isTraitable(): bool { return false; } @@ -287,12 +301,12 @@ class __TwigTemplate_%x extends Template /** * @codeCoverageIgnore */ - public function getDebugInfo() + public function getDebugInfo(): array { - return array ( 44 => 2, 42 => 4, 35 => 2,); + return array ( 48 => 2, 46 => 4, 39 => 2,); } - public function getSourceContext() + public function getSourceContext(): Source { return new Source("{{ foo }}", "foo.twig", ""); } diff --git a/tests/Node/TypesTest.php b/tests/Node/TypesTest.php new file mode 100644 index 000000000..ff6c2dfac --- /dev/null +++ b/tests/Node/TypesTest.php @@ -0,0 +1,43 @@ + [ + 'type' => 'string', + 'optional' => false, + ], + 'bar' => [ + 'type' => 'int', + 'optional' => true, + ] + ]; + } + + public function testConstructor() + { + $types = $this->getValidMapping(); + $node = new TypesNode($types, 1); + + $this->assertEquals($types, $node->getAttribute('mapping')); + } + + public function getTests() + { + return [ + // 1st test: Node shouldn't compile at all + [ + new TypesNode($this->getValidMapping(), 1), + '' + ] + ]; + } +} diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 18e196161..66437d2d1 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -21,6 +21,7 @@ use Twig\Sandbox\SecurityError; use Twig\Sandbox\SecurityPolicy; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; class TemplateTest extends TestCase { @@ -425,22 +426,22 @@ class TemplateForTest extends Template return true; } - public function getTemplateName() + public function getTemplateName(): string { return $this->name; } - public function getDebugInfo() + public function getDebugInfo() : array { return []; } - public function getSourceContext() + public function getSourceContext() : Source { return new Source('', $this->getTemplateName()); } - protected function doGetParent(array $context) + protected function doGetParent(array $context): bool|string|Template|TemplateWrapper { return false; } diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php new file mode 100644 index 000000000..1d27d9d15 --- /dev/null +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -0,0 +1,69 @@ + false, 'autoescape' => false]); + $stream = $env->tokenize($source = new Source($template, '')); + $parser = new Parser($env); + + $typesNode = $parser->parse($stream)->getNode('body')->getNode('0'); + + self::assertEquals($expected, $typesNode->getAttribute('mapping')); + } + + public function getMappingTests(): array + { + return [ + // empty mapping + [ + '{% types {} %}', + [], + ], + + // simple + [ + '{% types {foo: "bar"} %}', + [ + 'foo' => ['type' => 'bar', 'optional' => false] + ], + ], + + // trailing comma + [ + '{% types {foo: "bar",} %}', + [ + 'foo' => ['type' => 'bar', 'optional' => false] + ], + ], + + // optional name + [ + '{% types {foo?: "bar"} %}', + [ + 'foo' => ['type' => 'bar', 'optional' => true] + ], + ], + + // multiple pairs, duplicate values + [ + '{% types {foo: "foo", bar?: "foo", baz: "baz"} %}', + [ + 'foo' => ['type' => 'foo', 'optional' => false], + 'bar' => ['type' => 'foo', 'optional' => true], + 'baz' => ['type' => 'baz', 'optional' => false] + ], + ], + ]; + } +}