{"id":24866919,"url":"https://github.com/alisqi/twigqi","last_synced_at":"2026-02-05T12:15:17.659Z","repository":{"id":226738868,"uuid":"765734093","full_name":"alisqi/TwigQI","owner":"alisqi","description":"TwigQI: Static code analysis for Twig templates","archived":false,"fork":false,"pushed_at":"2025-08-27T09:47:30.000Z","size":304,"stargazers_count":31,"open_issues_count":1,"forks_count":3,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-09-26T19:41:41.750Z","etag":null,"topics":["code-quality","static-code-analysis","twig","twig-extension"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alisqi.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-01T14:09:29.000Z","updated_at":"2025-09-26T08:08:53.000Z","dependencies_parsed_at":"2024-03-16T15:15:17.529Z","dependency_job_id":"8ddf4b13-89b1-45c1-8197-ca63e28ee929","html_url":"https://github.com/alisqi/TwigQI","commit_stats":null,"previous_names":["alisqi/twig-stan","alisqi/twigstan"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/alisqi/TwigQI","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alisqi%2FTwigQI","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alisqi%2FTwigQI/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alisqi%2FTwigQI/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alisqi%2FTwigQI/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alisqi","download_url":"https://codeload.github.com/alisqi/TwigQI/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alisqi%2FTwigQI/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279078156,"owners_count":26098459,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-15T02:00:07.814Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-quality","static-code-analysis","twig","twig-extension"],"created_at":"2025-02-01T01:55:38.574Z","updated_at":"2025-10-15T12:30:55.240Z","avatar_url":"https://github.com/alisqi.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TwigQI: Static code analysis for Twig templates\n\n[![License](https://img.shields.io/github/license/alisqi/TwigQI.svg)](https://github.com/alisqi/TwigQI/blob/main/LICENSE)\n[![PHP Version](https://img.shields.io/badge/php-%3E%3D%208.2-8892BF.svg)](https://php.net)\n![Stability](https://img.shields.io/badge/stability-stable-brightgreen)\n[![Build Status](https://github.com/alisqi/TwigQI/actions/workflows/test.yml/badge.svg)](https://github.com/alisqi/TwigQI/actions)\n[![codecov](https://codecov.io/gh/alisqi/TwigQI/graph/badge.svg?token=G3AE3RE4K0)](https://codecov.io/gh/alisqi/TwigQI)\n\nTwig Quality Inspections is an extension to the [Twig templating](https://github.com/twigphp/Twig) engine\nwhich adds static analysis (i.e., compile-time) inspections and runtime assertions to increase templates' quality.\nSee the [inspections section](#Inspections) below for details.\n\n\n\nThe two intended use cases are:\n* Add the extension to the `Twig\\Environment` during development\n* Invoke a CLI command in CI and/or pre-commit hook which compiles all templates with the extension enabled.\n\n# Justification\nJust in case you need convincing, please consider the following example:\n\n```twig\n{% macro userCard(user, showBadge = false) %}\n  {{ user.name }}\n  {% if showBadge %}\n    {% if usr.admin %} {# Oops #}\n      (admin)\n    {% else if user.role %}\n      ({{ user.getRoleLabel(usr.role) }}) {# Uh oh! #}\n    {% endif %}\n  {% endif %}\n{% endmacro %}\n```\n\nHere, `usr.admin` is obviously a typo. Fortunately, this bug is easily detected with `strict_types` enabled,\nbut only if the macro is called with `showBadge=true`. In this example, the `(admin)` badge will simply never be printed in production\n(where `strict_types` is likely disabled).\n\nHowever, `user.getRoleLabel(usr.role)` will cause an uncaught `TypeError` if that method's parameter is not nullable,\nsince Twig will call that method with `null`. Instead of just having a buggy badge, *the whole page breaks*.\n\n# Installation\nFirst, install using\n```bash\ncomposer require --dev alisqi/twigqi\n```\n\n## Symfony integration\nIn a Symfony application, the recommended way is to create a class that extends `AlisQI\\TwigQI\\Extension` and add the `When` attribute.\nThis allows you to configure which inspections to enable.\n\n```php\n// src/Twig/TwigQIExtension.php\n\u003c?php\n\nnamespace App\\Twig;\n\nuse AlisQI\\TwigQI\\Extension;\nuse Symfony\\Component\\DependencyInjection\\Attribute\\When;\n\n#[When('dev')]\nfinal class TwigQIExtension extends AbstractExtension\n{\n    public function getNodeVisitors(): array\n    {\n        return [\n            // Assertions:\n            new WrapTypesInAssertedTypes(),\n\n            // Inspections:\n            // new BadArgumentCountInMacroCall(), // Kills the Web Debug Toolbar: https://github.com/alisqi/TwigQI/issues/3#issuecomment-2651503912\n            new InvalidConstant(),\n            new InvalidDotOperation(),\n            new InvalidTypes(),\n            new PositionalMacroArgumentAfterNamed(),\n            new RequiredMacroArgumentAfterOptional(),\n            // new UndeclaredVariableInMacro(), // Kills the Web Debug Toolbar: https://github.com/alisqi/TwigQI/issues/3#issuecomment-2651503912\n        ];\n    }\n}\n```\n\nAlternatively, if you want *all* inspections, you can enable the extension in your `config/services.yaml`:\n\n```yaml\nwhen@dev:\n    services:\n        AlisQI\\TwigQI\\Extension:\n            autowire: true\n            tags: [ 'twig.extension' ]\n```\n\n### Output \u0026 Logging\nEither way, all inspection results will [show up](docs/error-on-page.png) in the Web Debug Toolbar's logs.\n\n![Error shown in Web Debug Toolbar](docs/error-in-wdt.png)\n\nThis example is based on the [Symfony demo application](https://github.com/symfony/demo), where\n`src/templates/blog/post_show.html.twig` was amended to include the following in the `main` block:\n```twig\n{% types post: '\\\\App\\\\Entity\\\\Post' %}\n{% if false %} {# to demonstrate static typing during template compilation #}\n    {{ post.tilte }}\n{% endif %}\n```\n\n## Non-Symfony projects\nYou can also add the extension manually to your `Twig\\Environment`:\n```php\n$twig-\u003eaddExtension(new AlisQI\\TwigQI\\Extension($logger));\n```\n\n## Configuration\n\n### Logging\nThe extension class requires a `\\Psr\\Log\\LoggerInterface` implementation.\n\nThis package includes the `TriggerErrorLogger` class, which reports issues using PHP's `trigger_error()`\nwith appropriate `E_USER_*` levels.\n\n# Design\nThe current design uses `NodeVisitor` classes for every inspection. That allows for easy testing and configurability.\n\nThe level of error (error, warning, notice) depends entirely on the authors' opinions on code quality. `LogLevel::ERROR`\nis used for, well, errors, that the author(s) deem actual errors in code. `LogLevel::WARNING` is used for more\nopinionated issues, such as relying on macro arguments always being optional.\n\n## Typing system and syntax\nMany inspections rely on proper typing. However, the [documentation for the `types` tag](https://twig.symfony.com/doc/3.x/tags/types.html)\nexplicitly avoids specifying the syntax or contents of types.\n\nSo how should developers declare types? While PHP developers are often familiar with PHPStan, Twig template designers\nmay instead be used to TypeScript.\n\nThe [Twig documentation](https://twig.symfony.com/doc/3.x/templates.html#variables) sums up its stance succinctly:\n\n\u003e Twig tries to abstract PHP types as much as possible and works with a few basic types[.]\n\nTherefore, TwigQI uses the basic types described by Twig, while defining syntax for iterables. The goal is to have a\n*simple* type system that's easy to learn and use, and which should cover the vast majority of use cases.\n\nYour preferences and/or requirements may very well differ.\n\nHere's the list of types supported by TwigQI:\n* Scalar: `string`, `number`, `boolean`, `null`, `object` (although a class is preferred)\n* Classes, interfaces and traits\n\n  FQNs _may_ contain a leading backslash. Note that backslashes must be escaped in Twig strings [until v4](https://github.com/twigphp/Twig/pull/4199).\n* Three types of iterables, with increasing specificity\n  * `iterable` declares nothing more or less than that the variable is iterable\n  * `iterable\u003cValueType\u003e` declares the values' type\n  * `iterable\u003cnumber, ValueType\u003e` and `iterable\u003cstring, ValueType\u003e` does the same for keys\n  \n  You can create recursive types: `iterable\u003cstring, iterable\u003cnumber, iterable\u003cstring\u003e\u003e\u003e`\n* Lastly, `mixed` allows you to declare that a variable is defined without specifying a concrete type.\n\nAny type can be prefixed with `?` to make it nullable.\n\nNote that there's no dedicated syntax for iterables with particular, known keys. Nor can you declare that values have\ndifferent types. You could use one of the `iterable` variants (e.g., `iterable\u003cstring, mixed\u003e`), but I would humbly\nrecommend using a `readonly class` to act as a view model.\n\n# Inspections\nHere's the list of inspections already considered relevant and feasible.\n\nThose marked with ⌛ are planned / considered, while ✅ means the inspection is implemented.\n\nNote that most of these could also be analyzed by PHPStan if it could properly understand (compiled) templates and how\nthey are rendered. This is the aim of a similar project: [TwigStan](https://github.com/twigstan/twigstan).\n\n## Typed variables\n* ✅ Declared types is invalid (e.g., `{% types {i: 'nit'} %}`)\n* ✅ Runtime: non-optional variable is not defined\n* ✅ Runtime: non-nullable variable is null\n* ✅ Runtime: variable does not match type\n* ✅ Invalid object property or method (e.g., `{{ user.nmae }}`)\n  \n  Types for keys and values in `for` loops are automatically derived from iterable types.\n\n  ⚠️ This inspection _can_ trigger false positives, depending on your template logic.\n* ⌛ Undeclared variable (i.e., missing in `types`, `set`, etc)\n\n## Constants and enum cases\n* ✅ Invalid constant (e.g., `constant('BAD')`)\n* ✅ Expressions as first argument (e.g., `constant('UH' ~ 'OH')`)\n \n  This is opinionated, as it can work perfectly fine\n* ✅ Second argument (object) is not a name (e.g., `constant('CONST', {})`)\n  \n  This is opinionated, too: `constant('CONST', foo ?: bar)` can work fine\n\n* ✅ Invalid enum case (e.g., `enum('\\\\Some\\\\Enum').InvalidCase`)\n\n## Macros\nWhile Twig considers all macro arguments optional (and provides `null` as a default), TwigQI considers arguments with\nno explicit default value as required.\n\n* ✅ Undefined variable used (arguments, `{% set %}`, etc)\n* ✅ Call with *too many* arguments (except if `varargs` is used)\n* ✅ Call with *too few* arguments\n* ✅ Required argument declared after optional\n* ✅ Positional argument after named in call expression\n* ✅ Invalid named argument in call expression\n* ⌛ Arguments not declared using `types`\n* ⌛ Type mismatch in macro call\n\n# Similar Projects\n\n* [curlylint](https://www.curlylint.org/): Focuses on HTML\n* [djLint](https://www.djlint.com/docs/linter/): Focuses on HTML\n\n# Acknowledgments\nBig thanks to [Ruud Kamphuis](https://github.com/ruudk) for [TwigStan](https://github.com/twigstan/twigstan),\nand for helping on this very project.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falisqi%2Ftwigqi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falisqi%2Ftwigqi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falisqi%2Ftwigqi/lists"}