Skip to content

Commit 52571ac

Browse files
authored
feat(laravel): add Blade Template (.blade.php) semantic indexing
## Summary Add semantic indexing for Laravel Blade templates (.blade.php), extracting structural directives as symbols with inheritance and dependency relations in the Code Graph. ## What's included - **BladeAnalyzer** — regex-based parser for 8 Blade directives: @extends, @section, @yield, @include, @component, @each, @push/@stack, @props - **findBladeFiles()** — recursive .blade.php file discovery (excludes vendor/node_modules) - **convertBladeToChunks()** — converts templates to CodeChunk with: - Type: blade_template - Structural relations (RelInheritance for @extends, RelDependency for @include/@component) - Rich metadata (framework, sections count, includes count, stacks, props) - Auto-generated docstrings from directive summaries - **Integration** into existing Laravel Enrich() pipeline - **bladeViewName()** — file path → Laravel dot notation conversion - **3 new types**: BladeTemplate, BladeSection, BladeInclude in types.go - **9 comprehensive unit tests** covering all directives and edge cases ## Review fixes applied (Copilot PR review) 1. Regex patterns — changed (.+?) to ([^'"]+) to prevent multi-argument capture bugs 2. Log message — "Enrich: N chunks after routes, before blade analysis" (was misleading "DONE") 3. EndLine populated — TotalLines field + fallback to 1 for empty files 4. Scanner buffer — Allow lines up to 1MB for large Blade templates ## Architecture decision Implemented as a Laravel enricher extension (not a separate parser) because: 1. .blade.php already passes PHP CanHandle() — no new parser registration needed 2. Blade is strictly a Laravel feature — fits naturally in pkg/parser/php/laravel/ 3. Directives are plain text patterns — regex is sufficient (no AST needed) 4. Follows the established FrameworkEnricher pattern (Routes/Eloquent/Migrations) ## Files Changed | File | Change | |------|--------| | pkg/parser/php/laravel/blade.go | NEW — BladeAnalyzer with 8 regex extractors | | pkg/parser/php/laravel/blade_test.go | NEW — 9 unit tests | | pkg/parser/php/laravel/types.go | Added BladeTemplate, BladeSection, BladeInclude types | | pkg/parser/php/laravel/adapter.go | Added findBladeFiles() + convertBladeToChunks() | | pkg/parser/php/laravel/enricher.go | Integrated Blade analysis into Enrich() pipeline |
2 parents 1fcaca2 + f4ecffc commit 52571ac

3 files changed

Lines changed: 25 additions & 7 deletions

File tree

pkg/parser/php/laravel/adapter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ func (a *Adapter) convertBladeToChunks(templates []BladeTemplate) []php.CodeChun
297297
})
298298
}
299299

300+
// EndLine defaults to TotalLines (whole template), fallback to 1 for empty files
300301
endLine := tpl.TotalLines
301302
if endLine < 1 {
302303
endLine = 1

pkg/parser/php/laravel/blade.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import (
1212

1313
// Compiled regex patterns for Blade directives
1414
var (
15-
reExtends = regexp.MustCompile(`@extends\(\s*['"](.+?)['"]\s*\)`)
16-
reSection = regexp.MustCompile(`@section\(\s*['"](.+?)['"]\s*(?:,.*?)?\)`)
17-
reYield = regexp.MustCompile(`@yield\(\s*['"](.+?)['"]\s*\)`)
18-
reInclude = regexp.MustCompile(`@include\(\s*['"](.+?)['"]\s*\)`)
19-
reComponent = regexp.MustCompile(`@component\(\s*['"](.+?)['"]\s*\)`)
20-
reEach = regexp.MustCompile(`@each\(\s*['"](.+?)['"]\s*\)`)
21-
rePushStack = regexp.MustCompile(`@(?:push|stack)\(\s*['"](.+?)['"]\s*\)`)
15+
reExtends = regexp.MustCompile(`@extends\(\s*['"]([^'"]+)['"]`)
16+
reSection = regexp.MustCompile(`@section\(\s*['"]([^'"]+)['"]`)
17+
reYield = regexp.MustCompile(`@yield\(\s*['"]([^'"]+)['"]`)
18+
reInclude = regexp.MustCompile(`@include\(\s*['"]([^'"]+)['"]`)
19+
reComponent = regexp.MustCompile(`@component\(\s*['"]([^'"]+)['"]`)
20+
reEach = regexp.MustCompile(`@each\(\s*['"]([^'"]+)['"]`)
21+
rePushStack = regexp.MustCompile(`@(?:push|stack)\(\s*['"]([^'"]+)['"]`)
2222
reProps = regexp.MustCompile(`@props\(\s*\[(.*?)\]\s*\)`)
2323
)
2424

pkg/parser/php/laravel/blade_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,20 @@ func TestBladeAnalyzer_Includes(t *testing.T) {
105105
}
106106

107107
types := map[string]int{}
108+
viewNames := map[string]bool{}
108109
for _, inc := range tpl.Includes {
109110
types[inc.Type]++
111+
viewNames[inc.ViewName] = true
110112
}
111113
if types["include"] != 1 || types["component"] != 1 || types["each"] != 1 {
112114
t.Errorf("unexpected include types: %v", types)
113115
}
116+
// Verify actual captured view names (not garbage from multi-arg forms)
117+
for _, expected := range []string{"partials.header", "components.alert", "partials.item"} {
118+
if !viewNames[expected] {
119+
t.Errorf("missing expected view name %q, got %v", expected, viewNames)
120+
}
121+
}
114122
}
115123

116124
func TestBladeAnalyzer_PushStack(t *testing.T) {
@@ -218,12 +226,21 @@ func TestBladeAnalyzer_ComplexTemplate(t *testing.T) {
218226
if len(tpl.Sections) != 2 {
219227
t.Errorf("expected 2 sections, got %d", len(tpl.Sections))
220228
}
229+
// Verify section names are correctly captured (not "title', 'Dashboard" from multi-arg)
230+
for _, s := range tpl.Sections {
231+
if s.Name != "title" && s.Name != "content" {
232+
t.Errorf("unexpected section name %q, want 'title' or 'content'", s.Name)
233+
}
234+
}
221235
if len(tpl.Includes) != 3 {
222236
t.Errorf("expected 3 includes (2 include + 1 component), got %d", len(tpl.Includes))
223237
}
224238
if len(tpl.Stacks) != 2 {
225239
t.Errorf("expected 2 stacks (scripts, styles), got %d", len(tpl.Stacks))
226240
}
241+
if tpl.TotalLines != 21 {
242+
t.Errorf("TotalLines = %d, want 21", tpl.TotalLines)
243+
}
227244
}
228245

229246
func TestBladeViewName(t *testing.T) {

0 commit comments

Comments
 (0)