From 494c08452ca1c71cf480bc5b547dcbc3eff120b7 Mon Sep 17 00:00:00 2001 From: rajatnagda45 Date: Tue, 29 Sep 2026 01:49:35 +0530 Subject: [PATCH] fix(blade): link a view to the layout it @extends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Blade extractor recognised @include, 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 efeeeefa53f1c14851c028ea3d163b3b25e4f180) --- graphify/extractors/blade.py | 16 +++++++++++++++- tests/test_blade_extractor.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/test_blade_extractor.py diff --git a/graphify/extractors/blade.py b/graphify/extractors/blade.py index 63e4fac9a..73101f0c5 100644 --- a/graphify/extractors/blade.py +++ b/graphify/extractors/blade.py @@ -7,7 +7,7 @@ from graphify.extractors.base import _make_id def extract_blade(path: Path) -> dict: - """Extract @include, components, and wire:click bindings from Blade templates.""" + """Extract @extends, @include, 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(".", "/") diff --git a/tests/test_blade_extractor.py b/tests/test_blade_extractor.py new file mode 100644 index 000000000..a3dffd3d0 --- /dev/null +++ b/tests/test_blade_extractor.py @@ -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")