fix(blade): link a view to the layout it @extends

The Blade extractor recognised @include, <livewire:> components, and
wire:click bindings, but not @extends. Template inheritance is the primary
structural relationship in a Blade view — most page templates render inside
a layout via @extends('layouts.app') — so the child was left with no edge
to its parent layout in the graph.

Emit an 'extends' edge from the view to the named layout, mirroring the
existing @include handling (dotted view name -> path). Adds the first test
coverage for the Blade extractor.

(cherry picked from commit efeeeefa53)
This commit is contained in:
rajatnagda45 authored and safishamsi committed 2026-09-29 13:49:39 +01:00
1 parent 4f6a4efd1f
commit 494c08452c
2 files changed
+48 -1

No files matched your search

+15 -1
View File
@@ -7,7 +7,7 @@ from graphify.extractors.base import _make_id
def extract_blade(path: Path) -> dict:
"""Extract @include, <livewire:> components, and wire:click bindings from Blade templates."""
"""Extract @extends, @include, <livewire:> components, and wire:click bindings from Blade templates."""
import re
try:
src = path.read_text(encoding="utf-8", errors="replace")
@@ -19,6 +19,20 @@ def extract_blade(path: Path) -> dict:
"source_file": str(path), "source_location": None}]
edges = []
# @extends('layouts.app') — template inheritance: this view renders inside
# the named parent layout. It is the primary structural relationship in a
# Blade view (most page templates extend a layout), so dropping it left the
# child disconnected from its layout in the graph.
for m in re.finditer(r"@extends\(['\"]([^'\"]+)['\"]", src):
tgt = m.group(1).replace(".", "/")
tgt_nid = _make_id(tgt)
if tgt_nid not in {n["id"] for n in nodes}:
nodes.append({"id": tgt_nid, "label": m.group(1), "file_type": "code",
"source_file": str(path), "source_location": None})
edges.append({"source": file_nid, "target": tgt_nid, "relation": "extends",
"confidence": "EXTRACTED", "confidence_score": 1.0,
"source_file": str(path), "source_location": None, "weight": 1.0})
# @include('path.to.partial') or @include("path.to.partial")
for m in re.finditer(r"@include\(['\"]([^'\"]+)['\"]", src):
tgt = m.group(1).replace(".", "/")
+33
View File
@@ -0,0 +1,33 @@
"""Extraction coverage for Laravel Blade templates."""
from __future__ import annotations
from graphify.extract import extract
def _edge_labels(result: dict, relation: str) -> set[tuple[str, str]]:
labels = {node["id"]: node["label"] for node in result["nodes"]}
return {
(labels.get(edge["source"], edge["source"]), labels.get(edge["target"], edge["target"]))
for edge in result["edges"]
if edge["relation"] == relation
}
def test_blade_extends_links_view_to_its_layout(tmp_path):
# @extends is template inheritance — the primary structural relationship in a
# Blade view. Before the fix only @include was recognised, so a page that
# extends a layout produced no edge to that layout at all.
page = tmp_path / "page.blade.php"
page.write_text(
"@extends('layouts.app')\n"
"@section('content')\n"
" @include('partials.nav')\n"
"@endsection\n",
encoding="utf-8",
)
result = extract([page], cache_root=tmp_path)
assert ("page.blade.php", "layouts.app") in _edge_labels(result, "extends")
# existing @include behaviour is preserved
assert ("page.blade.php", "partials.nav") in _edge_labels(result, "includes")