Skip to content

feat: Twig templates and controller-to-template links - #33

Merged
alexwrite merged 5 commits into
mainfrom
feat/twig-templates
Oct 11, 2026
Merged

alexwrite merged 5 commits into
mainfrom
feat/twig-templates

Conversation

@alexwrite

Copy link
Copy Markdown
Owner

Closes #19

Problem

.twig files were in no FileType set: 0 nodes for the 130 templates of project B (Symfony 8.1), so the Route -> Controller -> ... -> view chain stopped at the controller and nothing said which pages a template change touches.

Implementation

Commit Author What
feat(extract): add a Twig template extractor Andy Slezak (Graphify-Labs#2240) Twig part of Graphify-Labs#2240, SCSS dropped (separate concern)
fix(extract): keep template composition edges alive through id disambiguation Andy Slezak (Graphify-Labs#2240) idem
fix(twig): resolve names against the loader root only fail-closed resolution (below)
feat(php): link controllers to the Twig template they render renders edge, shared mechanism in extractors/php_views.py
docs(changelog) Unreleased entry

Why Graphify-Labs#2240 and not Graphify-Labs#2001 (both upstream, open), measured on project B with the same base:

Graphify-Labs#2001 Graphify-Labs#2240
.twig file nodes 113 / 130 130 / 130
extends to the real file 0 / 79 79 / 79
include to the real file 0 / 22 22 / 22

Graphify-Labs#2001 mints a node with the same id as the target file node, so _disambiguate_colliding_node_ids salts both apart and every link lands on a copy. Graphify-Labs#2240 stamps target_file on the edge and mints no node (the Graphify-Labs#2195 pattern).

Changes on top of Graphify-Labs#2240, all fail-closed:

  • A bare name resolves against the loader root only (a template's enclosing templates/ dir, else the nearest templates/ child walking up). feat(extract): add Twig and SCSS extractors Graphify-Labs/graphify#2240 tried the referring file's own directory first, which Twig never does; a same-named sibling won.
  • @Namespace/x (Symfony bundle or configured path) stays a stub. feat(extract): add Twig and SCSS extractors Graphify-Labs/graphify#2240 stripped the namespace and matched the remainder against the app's templates. Drupal @extension/x resolves to <extension>/templates/x then <extension>/x, as Drupal core FilesystemLoader does; a foreign extension's name no longer self-loops onto the local override.
  • renders edge from the PHP method to the template file for a literal name in render(), renderView(), renderBlock(), renderBlockView() (any receiver, name ending in .twig) and #[Template('...')] / #[Template(template: '...')]. No edge for a concatenated or interpolated name, or a name with no file. extractors/php_views.py holds the detection and an engine -> resolver table, so the Blade PR plugs into it instead of adding a second mechanism. renders joins _TEMPLATE_TARGET_RELATIONS and the engine's cross-file edge allowlist.
  • _AST_CACHE_SCHEMA 8 -> 9: cached PHP extractions would otherwise never get the new edges.

Twig name semantics: https://symfony.com/doc/current/templates.html ("the template path is the relative file path from templates/"; bundle namespaces). Drupal: core/lib/Drupal/Core/Template/Loader/FilesystemLoader.php (addPath($path . '/templates', $name) then addPath($path, $name)).

Verification

Commands run in a dedicated worktree on this branch:

  • uv run pytest tests/ -q: 7550 passed, 14 skipped (base origin/main: 7528 passed, 14 skipped).
  • uv run ruff check .: All checks passed!
  • uv run pyright on every touched module: 199 errors on the branch, 199 on origin/main, none new (per-message differential).
  • Regression tests seen failing first: 4 resolution tests in tests/test_twig.py (sibling file, nested templates/, bundle namespace, foreign Drupal namespace), the PHP-under-a-templates/-dir test, and test_symfony_render_and_template_attribute_link_to_the_template_file (empty set without the engine wiring).
  • Project B (graphify extract --code-only): 130/130 template nodes, 79/79 extends and 22/22 include on real files (1 stub: form_div_layout.html.twig, from the vendor bridge), 3 dynamic includes with no edge, 44 renders edges. 44 is the count of unique (file, method, template) for the 51 literal render calls, checked with an independent script.

Limitations

  • Twig components (<twig:Name>, component()) are not linked.
  • twig.default_path and twig.paths from twig.yaml are not read: the default templates/ is assumed.
  • TemplatedEmail::htmlTemplate() / textTemplate() are not followed.

amslezak and others added 5 commits October 11, 2026 13:21
Twig templates were in no FileType set, so they were silently skipped and
the view layer of a Symfony, Drupal or Craft project was missing from the
graph.

Regex extractor in the shape of extractors/blade.py: includes, embeds,
extends, imports_macro, uses_template, defines_block, attaches_library.
References to a real file get an edge stamped with target_file and no stub
node; Drupal SDC references resolve through <provider>.info.yml.
Comments are blanked before scanning so docblock usage examples are not
read as dependencies.

Cherry-picked from Graphify-Labs#2240 (c61df7c), Twig part only:
the SCSS extractor of that PR is a separate concern and is left out.

Refs #19
…iguation

When two files share a stem (a component's card.twig next to another
extracted card.* file), _disambiguate_colliding_node_ids salts their ids
apart, and an include edge from a third template no longer found its
target. Mirror the Graphify-Labs#1475 C/ObjC header fix: repoint template-composition
edges to the .twig variant, and honor the target_file stamp for those
relations.

Cherry-picked from Graphify-Labs#2240 (234579d), Twig part only.

Refs #19
Twig names are relative to the loader root, never to the referring file,
so a same-named file next to the referrer, or a nested templates/ dir, no
longer wins. A Symfony @Namespace/ is a bundle or a configured path and
stays a stub instead of being matched against the app templates; Drupal's
@extension/ resolves under that extension, as core FilesystemLoader does.

Refs #19

Co-Authored-By: Claude <noreply@anthropic.com>
A literal template name passed to render(), renderView(), renderBlock(),
renderBlockView() or #[Template(...)] now yields a `renders` edge from the
method to the template file node, resolved against the app's templates/
dir. A name built at runtime, or one with no file on disk, yields no edge.
The mechanism lives in extractors/php_views.py so other template engines
can plug their resolver into it. AST cache schema bumped so cached PHP
extractions pick the new edges up.

Refs #19

Co-Authored-By: Claude <noreply@anthropic.com>
Refs #19

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @alexwrite. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Twig : prendre en charge les templates et le lien contrôleur vers vue

2 participants