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")