Skip to content

TwigStan is a static analyzer for Twig templates powered by PHPStan

License

Notifications You must be signed in to change notification settings

twigstan/twigstan

Repository files navigation

Logo
TwigStan is a static analyzer for Twig templates powered by PHPStan.

Screenshot

Latest Stable Version PHP Version Require Total Downloads License


Caution

This is very experimental

Introduction

TwigStan converts Twig templates into simplified PHP code, allowing PHPStan to analyze them. It then reports any errors back to the original template and line number.

The process consists of the following steps:

Compilation

The TwigCompiler loads the template and converts it into a Twig AST (Abstract Syntax Tree). The AST is optimized by running several Twig NodeVisitors. The AST is then compiled into PHP using Twig's default compiler. The compiled PHP code is loaded and converted into a PHP AST. On the PHP AST, we run various PHP NodeVisitors. The goal is no longer to render the template but to analyze it. This means we can remove elements that are not relevant to us. The PHP AST is then dumped back into PHP code and saved to disk as a compilation result.

In the next steps, we will use these PHP files.

Flattening

The next step is to flatten the Twig templates. Templates can extend other templates. The child template can choose to override blocks or not, and the parent template can also extend another template. Variables set in a parent template should be available in the child template.

The TwigFlattener processes all the compilation results. It reads the Twig metadata to identify the parent(s) and defined blocks. It takes the logic in the parent template (set variables, etc.) from the doDisplay method and copies it into the child template's doDisplay block.

The same is done for the block hierarchy. It understands which blocks are overridden. The child template will eventually have all blocks defined.

While flattening, the original filename and line numbers are preserved. This is important because later on, we want to trace errors back to their original location.

After the flattening process is finished, the PHP AST is again dumped to disk as a flattening result.

Scope Collecting

Now that we have a flat template, we don't know anything about the context the template receives or the modified context inside the template.

We use PHPStan to run the BlockContextCollector. This collector gathers the context before rendering every block or parent block call.

While running PHPStan, it's also a good time to search for places that render the template.

Scope Injection

Now that we know the context passed to a template, and the context before every block call in the template, we can inject this knowledge as PHPDocs into the flattened template.

Analysis

Every template is now flattened and has defined context types.

We ask PHPStan to run the analysis on these files.

The AnalysisResultFromJsonReader processes the results from PHPStan. For every error in the flattened PHP code, it tries to find the original Twig file and line number. It filters out a few errors that are false positives. It also collapses errors that are already reported higher in the hierarchy. When an error is reported in a parent template, it should only be reported once, instead of every time it's flattened in a child template.

Installation

$ composer require --dev twigstan/twigstan:dev-main

Then run TwigStan and it will explain what to do next:

$ vendor/bin/twigstan

Usage

Defining types

TwigStan supports the new {% types %} tag that will be introduced in Twig 3.13.

If your types are not automatially resolved from where they are rendered, you manually type each and every variable like t

{% types { variableName: 'type' } %}

The type can be a valid PHPDoc expression. For example:

{% types { name: 'string|null' } %}

Next to using multiple {% types %} tags, you can also define multiple types in a single line:

{% types {
    name: 'string',
    users: 'array<int, App\\User>',
} %}

If you want to indicate that a variable is optional, you can do it as follows:

{% types {
    isEnabled?: 'bool',
} %}

Note

Starting from Twig version 4 you no longer have to escape backslashes in fully qualified class names.

Debugging

You can dump the type of a variable by using:

{% dump_type variableName %}

When running TwigStan it will then output the type of the variable at that point.

For example:

{% types { authenticated:  'bool' } %}

This will print `bool`:
{% dump_type authenticated %}

{% if authenticated %}
    This will print `true`:
    {% dump_type authenticated %}
{% else %}
    This will print `false`:
    {% dump_type authenticated %}
{% endif %}

If you want to dump the types for the whole context (everything that's available), you can do:

{% dump_type %}

Known issues / todo

  • Macros are not yet supported
  • Horizontal reuse is not yet supported
  • Dynamic inheritance is not supported
  • Conditional inheritence is not yet supported
  • Not all render points are detected (currently only supports Symfony controllers)
  • Performance (PHPStan's cache misses all the time, should be fixed to speed things up significantly)
  • Baseline is missing
  • PHPStan extension installer is not yet supported

Credits & Inspiration

About

TwigStan is a static analyzer for Twig templates powered by PHPStan

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Contributors 4

  •  
  •  
  •  
  •