diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0eb1551..2e9f5ac 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -40,7 +40,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
- go-version: '1.22'
+ go-version: '1.24'
cache: true
- name: Run GoReleaser
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index eef1806..f07bfd7 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,7 +18,7 @@
- name: Set up Go
uses: actions/setup-go@v5
with:
- go-version: '1.22'
+ go-version: '1.24'
cache: true
- name: Download dependencies
@@ -54,7 +54,7 @@
- name: Set up Go
uses: actions/setup-go@v5
with:
- go-version: '1.22'
+ go-version: '1.24'
cache: true
- name: golangci-lint
diff --git a/.gitignore b/.gitignore
index 62e4365..9d85bac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,6 +60,7 @@ Thumbs.db
# Temporary files
tmp/
temp/
+docs/plans/*.md
# Scripts
scripts/
diff --git a/pkg/parser/php/laravel/adapter.go b/pkg/parser/php/laravel/adapter.go
index 554d1f7..c1971ac 100644
--- a/pkg/parser/php/laravel/adapter.go
+++ b/pkg/parser/php/laravel/adapter.go
@@ -3,6 +3,7 @@ package laravel
import (
"fmt"
"io/fs"
+ "os"
"path/filepath"
"strings"
@@ -199,3 +200,135 @@ func (a *Adapter) convertRoutesToChunks(routes []Route) []php.CodeChunk {
return chunks
}
+
+// findBladeFiles searches recursively for .blade.php files in the given paths.
+// Skips vendor/, node_modules/, .git/, and hidden directories.
+func (a *Adapter) findBladeFiles(paths []string) []string {
+ var bladeFiles []string
+
+ for _, root := range paths {
+ info, err := os.Stat(root)
+ if err != nil {
+ continue
+ }
+
+ // Single file check
+ if !info.IsDir() {
+ if strings.HasSuffix(root, ".blade.php") {
+ bladeFiles = append(bladeFiles, root)
+ }
+ continue
+ }
+
+ // Walk directory
+ _ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return nil
+ }
+ if d.IsDir() {
+ base := d.Name()
+ if base == "vendor" || base == "node_modules" || base == ".git" ||
+ strings.HasPrefix(base, ".") {
+ if path != root {
+ return filepath.SkipDir
+ }
+ }
+ return nil
+ }
+ if strings.HasSuffix(d.Name(), ".blade.php") {
+ bladeFiles = append(bladeFiles, path)
+ }
+ return nil
+ })
+ }
+
+ return bladeFiles
+}
+
+// convertBladeToChunks converts BladeTemplate structs to php.CodeChunk with
+// metadata and structural relations (inheritance for @extends, dependency for @include/@component).
+func (a *Adapter) convertBladeToChunks(templates []BladeTemplate) []php.CodeChunk {
+ var chunks []php.CodeChunk
+
+ for _, tpl := range templates {
+ // Build signature
+ sig := tpl.Name
+ if tpl.Extends != "" {
+ sig = fmt.Sprintf("@extends('%s')", tpl.Extends)
+ }
+
+ // Build docstring from directives summary
+ var docParts []string
+ if tpl.Extends != "" {
+ docParts = append(docParts, fmt.Sprintf("Extends: %s", tpl.Extends))
+ }
+ if len(tpl.Sections) > 0 {
+ names := make([]string, len(tpl.Sections))
+ for i, s := range tpl.Sections {
+ names[i] = s.Name
+ }
+ docParts = append(docParts, fmt.Sprintf("Sections: %s", strings.Join(names, ", ")))
+ }
+ if len(tpl.Includes) > 0 {
+ names := make([]string, len(tpl.Includes))
+ for i, inc := range tpl.Includes {
+ names[i] = inc.ViewName
+ }
+ docParts = append(docParts, fmt.Sprintf("Includes: %s", strings.Join(names, ", ")))
+ }
+ if len(tpl.Props) > 0 {
+ docParts = append(docParts, fmt.Sprintf("Props: %s", strings.Join(tpl.Props, ", ")))
+ }
+
+ docstring := strings.Join(docParts, " | ")
+
+ // Build relations
+ var relations []pkgParser.Relation
+ if tpl.Extends != "" {
+ relations = append(relations, pkgParser.Relation{
+ TargetName: tpl.Extends,
+ Type: pkgParser.RelInheritance,
+ })
+ }
+ for _, inc := range tpl.Includes {
+ relations = append(relations, pkgParser.Relation{
+ TargetName: inc.ViewName,
+ Type: pkgParser.RelDependency,
+ })
+ }
+
+ endLine := tpl.TotalLines
+ if endLine < 1 {
+ endLine = 1
+ }
+
+ chunk := php.CodeChunk{
+ Name: tpl.Name,
+ Type: "blade_template",
+ Language: "php",
+ FilePath: tpl.FilePath,
+ StartLine: 1,
+ EndLine: endLine,
+ Signature: sig,
+ Docstring: docstring,
+ Metadata: map[string]any{
+ "framework": "laravel",
+ "blade": true,
+ "sections_count": len(tpl.Sections),
+ "includes_count": len(tpl.Includes),
+ },
+ Relations: relations,
+ }
+
+ if len(tpl.Stacks) > 0 {
+ chunk.Metadata["stacks"] = tpl.Stacks
+ }
+ if len(tpl.Props) > 0 {
+ chunk.Metadata["props"] = tpl.Props
+ }
+
+ chunks = append(chunks, chunk)
+ }
+
+ return chunks
+}
diff --git a/pkg/parser/php/laravel/blade.go b/pkg/parser/php/laravel/blade.go
new file mode 100644
index 0000000..9969d3c
--- /dev/null
+++ b/pkg/parser/php/laravel/blade.go
@@ -0,0 +1,181 @@
+package laravel
+
+import (
+ "bufio"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/doITmagic/rag-code-mcp/internal/logger"
+)
+
+// Compiled regex patterns for Blade directives
+var (
+ reExtends = regexp.MustCompile(`@extends\(\s*['"](.+?)['"]\s*\)`)
+ reSection = regexp.MustCompile(`@section\(\s*['"](.+?)['"]\s*(?:,.*?)?\)`)
+ reYield = regexp.MustCompile(`@yield\(\s*['"](.+?)['"]\s*\)`)
+ reInclude = regexp.MustCompile(`@include\(\s*['"](.+?)['"]\s*\)`)
+ reComponent = regexp.MustCompile(`@component\(\s*['"](.+?)['"]\s*\)`)
+ reEach = regexp.MustCompile(`@each\(\s*['"](.+?)['"]\s*\)`)
+ rePushStack = regexp.MustCompile(`@(?:push|stack)\(\s*['"](.+?)['"]\s*\)`)
+ reProps = regexp.MustCompile(`@props\(\s*\[(.*?)\]\s*\)`)
+)
+
+// BladeAnalyzer parses Blade template files and extracts directives.
+type BladeAnalyzer struct{}
+
+// NewBladeAnalyzer creates a new BladeAnalyzer.
+func NewBladeAnalyzer() *BladeAnalyzer {
+ return &BladeAnalyzer{}
+}
+
+// Analyze parses the given Blade template files, extracting directives.
+// Files that cannot be read are logged and skipped (no error returned).
+func (ba *BladeAnalyzer) Analyze(filePaths []string) []BladeTemplate {
+ var templates []BladeTemplate
+
+ for _, fp := range filePaths {
+ tpl, err := ba.analyzeFile(fp)
+ if err != nil {
+ logger.Instance.Debug("[BLADE] skip %s: %v", filepath.Base(fp), err)
+ continue
+ }
+ templates = append(templates, tpl)
+ }
+
+ return templates
+}
+
+// analyzeFile parses a single Blade file.
+func (ba *BladeAnalyzer) analyzeFile(filePath string) (BladeTemplate, error) {
+ f, err := os.Open(filePath)
+ if err != nil {
+ return BladeTemplate{}, err
+ }
+ defer f.Close()
+
+ tpl := BladeTemplate{
+ Name: bladeViewName(filePath),
+ FilePath: filePath,
+ }
+
+ scanner := bufio.NewScanner(f)
+ scanner.Buffer(make([]byte, 64*1024), 1024*1024) // Allow lines up to 1MB
+ lineNum := 0
+ for scanner.Scan() {
+ lineNum++
+ line := scanner.Text()
+
+ // @extends
+ if m := reExtends.FindStringSubmatch(line); len(m) > 1 {
+ tpl.Extends = m[1]
+ }
+
+ // @section
+ if m := reSection.FindStringSubmatch(line); len(m) > 1 {
+ tpl.Sections = append(tpl.Sections, BladeSection{
+ Name: m[1],
+ Type: "section",
+ StartLine: lineNum,
+ })
+ }
+
+ // @yield
+ if m := reYield.FindStringSubmatch(line); len(m) > 1 {
+ tpl.Sections = append(tpl.Sections, BladeSection{
+ Name: m[1],
+ Type: "yield",
+ StartLine: lineNum,
+ })
+ }
+
+ // @include
+ if m := reInclude.FindStringSubmatch(line); len(m) > 1 {
+ tpl.Includes = append(tpl.Includes, BladeInclude{
+ ViewName: m[1],
+ Type: "include",
+ Line: lineNum,
+ })
+ }
+
+ // @component
+ if m := reComponent.FindStringSubmatch(line); len(m) > 1 {
+ tpl.Includes = append(tpl.Includes, BladeInclude{
+ ViewName: m[1],
+ Type: "component",
+ Line: lineNum,
+ })
+ }
+
+ // @each
+ if m := reEach.FindStringSubmatch(line); len(m) > 1 {
+ tpl.Includes = append(tpl.Includes, BladeInclude{
+ ViewName: m[1],
+ Type: "each",
+ Line: lineNum,
+ })
+ }
+
+ // @push / @stack
+ if m := rePushStack.FindStringSubmatch(line); len(m) > 1 {
+ tpl.Stacks = appendUnique(tpl.Stacks, m[1])
+ }
+
+ // @props
+ if m := reProps.FindStringSubmatch(line); len(m) > 1 {
+ props := parsePropsArray(m[1])
+ tpl.Props = append(tpl.Props, props...)
+ }
+ }
+
+ tpl.TotalLines = lineNum
+
+ return tpl, scanner.Err()
+}
+
+// bladeViewName converts a file path to Laravel dot notation.
+// Example: /project/resources/views/layouts/app.blade.php → layouts.app
+func bladeViewName(filePath string) string {
+ // Normalize to forward slashes
+ fp := filepath.ToSlash(filePath)
+
+ // Try to find resources/views/ in the path
+ marker := "resources/views/"
+ idx := strings.LastIndex(fp, marker)
+ if idx >= 0 {
+ relative := fp[idx+len(marker):]
+ // Remove .blade.php extension
+ relative = strings.TrimSuffix(relative, ".blade.php")
+ return strings.ReplaceAll(relative, "/", ".")
+ }
+
+ // Fallback: use basename without extension
+ base := filepath.Base(filePath)
+ return strings.TrimSuffix(base, ".blade.php")
+}
+
+// parsePropsArray extracts prop names from a @props([...]) content string.
+// Input: "'title', 'color'" → Output: ["title", "color"]
+func parsePropsArray(raw string) []string {
+ var props []string
+ parts := strings.Split(raw, ",")
+ for _, p := range parts {
+ p = strings.TrimSpace(p)
+ p = strings.Trim(p, "'\"")
+ if p != "" {
+ props = append(props, p)
+ }
+ }
+ return props
+}
+
+// appendUnique appends s to slice only if not already present.
+func appendUnique(slice []string, s string) []string {
+ for _, existing := range slice {
+ if existing == s {
+ return slice
+ }
+ }
+ return append(slice, s)
+}
diff --git a/pkg/parser/php/laravel/blade_test.go b/pkg/parser/php/laravel/blade_test.go
new file mode 100644
index 0000000..b043d73
--- /dev/null
+++ b/pkg/parser/php/laravel/blade_test.go
@@ -0,0 +1,245 @@
+package laravel
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// writeTempBlade creates a temp .blade.php file inside resources/views/ and returns its path.
+func writeTempBlade(t *testing.T, dir, name, content string) string {
+ t.Helper()
+ viewsDir := filepath.Join(dir, "resources", "views")
+ fullPath := filepath.Join(viewsDir, name)
+ if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(fullPath, []byte(content), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ return fullPath
+}
+
+func TestBladeAnalyzer_Extends(t *testing.T) {
+ dir := t.TempDir()
+ fp := writeTempBlade(t, dir, "pages/home.blade.php",
+ `@extends('layouts.app')
+
Hello
`)
+
+ ba := NewBladeAnalyzer()
+ templates := ba.Analyze([]string{fp})
+
+ if len(templates) != 1 {
+ t.Fatalf("expected 1 template, got %d", len(templates))
+ }
+ tpl := templates[0]
+ if tpl.Extends != "layouts.app" {
+ t.Errorf("Extends = %q, want %q", tpl.Extends, "layouts.app")
+ }
+ if tpl.Name != "pages.home" {
+ t.Errorf("Name = %q, want %q", tpl.Name, "pages.home")
+ }
+}
+
+func TestBladeAnalyzer_Sections(t *testing.T) {
+ dir := t.TempDir()
+ fp := writeTempBlade(t, dir, "layouts/app.blade.php",
+ `
+@yield('title')
+
+@yield('content')
+@section('sidebar')
+ default sidebar
+@endsection
+
+`)
+
+ ba := NewBladeAnalyzer()
+ templates := ba.Analyze([]string{fp})
+
+ if len(templates) != 1 {
+ t.Fatalf("expected 1 template, got %d", len(templates))
+ }
+ tpl := templates[0]
+ if len(tpl.Sections) != 3 {
+ t.Fatalf("expected 3 sections, got %d: %+v", len(tpl.Sections), tpl.Sections)
+ }
+
+ yields := 0
+ sections := 0
+ for _, s := range tpl.Sections {
+ switch s.Type {
+ case "yield":
+ yields++
+ case "section":
+ sections++
+ }
+ }
+ if yields != 2 {
+ t.Errorf("expected 2 yields, got %d", yields)
+ }
+ if sections != 1 {
+ t.Errorf("expected 1 section, got %d", sections)
+ }
+}
+
+func TestBladeAnalyzer_Includes(t *testing.T) {
+ dir := t.TempDir()
+ fp := writeTempBlade(t, dir, "pages/show.blade.php",
+ `@extends('layouts.app')
+@include('partials.header')
+@component('components.alert')
+ Alert content
+@endcomponent
+@each('partials.item', $items, 'item')`)
+
+ ba := NewBladeAnalyzer()
+ templates := ba.Analyze([]string{fp})
+
+ if len(templates) != 1 {
+ t.Fatalf("expected 1 template, got %d", len(templates))
+ }
+ tpl := templates[0]
+ if len(tpl.Includes) != 3 {
+ t.Fatalf("expected 3 includes, got %d: %+v", len(tpl.Includes), tpl.Includes)
+ }
+
+ types := map[string]int{}
+ for _, inc := range tpl.Includes {
+ types[inc.Type]++
+ }
+ if types["include"] != 1 || types["component"] != 1 || types["each"] != 1 {
+ t.Errorf("unexpected include types: %v", types)
+ }
+}
+
+func TestBladeAnalyzer_PushStack(t *testing.T) {
+ dir := t.TempDir()
+ fp := writeTempBlade(t, dir, "layouts/app.blade.php",
+ `
+@stack('styles')
+
+
+@stack('scripts')
+`)
+
+ ba := NewBladeAnalyzer()
+ templates := ba.Analyze([]string{fp})
+
+ if len(templates) != 1 {
+ t.Fatalf("expected 1 template, got %d", len(templates))
+ }
+ if len(templates[0].Stacks) != 2 {
+ t.Errorf("expected 2 stacks, got %d: %v", len(templates[0].Stacks), templates[0].Stacks)
+ }
+}
+
+func TestBladeAnalyzer_Props(t *testing.T) {
+ dir := t.TempDir()
+ fp := writeTempBlade(t, dir, "components/alert.blade.php",
+ `@props(['title', 'color', 'dismissible'])
+
+
{{ $title }}
+ {{ $slot }}
+`)
+
+ ba := NewBladeAnalyzer()
+ templates := ba.Analyze([]string{fp})
+
+ if len(templates) != 1 {
+ t.Fatalf("expected 1 template, got %d", len(templates))
+ }
+ if len(templates[0].Props) != 3 {
+ t.Errorf("expected 3 props, got %d: %v", len(templates[0].Props), templates[0].Props)
+ }
+}
+
+func TestBladeAnalyzer_EmptyFile(t *testing.T) {
+ dir := t.TempDir()
+ fp := writeTempBlade(t, dir, "empty.blade.php", "")
+
+ ba := NewBladeAnalyzer()
+ templates := ba.Analyze([]string{fp})
+
+ if len(templates) != 1 {
+ t.Fatalf("expected 1 template (even if empty), got %d", len(templates))
+ }
+ tpl := templates[0]
+ if tpl.Extends != "" || len(tpl.Sections) != 0 || len(tpl.Includes) != 0 {
+ t.Errorf("empty template should have no directives, got %+v", tpl)
+ }
+}
+
+func TestBladeAnalyzer_NonexistentFile(t *testing.T) {
+ ba := NewBladeAnalyzer()
+ templates := ba.Analyze([]string{"/nonexistent/file.blade.php"})
+
+ if len(templates) != 0 {
+ t.Errorf("expected 0 templates for nonexistent file, got %d", len(templates))
+ }
+}
+
+func TestBladeAnalyzer_ComplexTemplate(t *testing.T) {
+ dir := t.TempDir()
+ fp := writeTempBlade(t, dir, "pages/dashboard.blade.php",
+ `@extends('layouts.admin')
+
+@section('title', 'Dashboard')
+
+@section('content')
+
+ @include('partials.stats')
+ @include('partials.charts')
+ @component('components.card')
+
Welcome
+ @endcomponent
+
+@endsection
+
+@push('scripts')
+
+@endpush
+
+@push('styles')
+
+@endpush`)
+
+ ba := NewBladeAnalyzer()
+ templates := ba.Analyze([]string{fp})
+
+ if len(templates) != 1 {
+ t.Fatalf("expected 1 template, got %d", len(templates))
+ }
+ tpl := templates[0]
+
+ if tpl.Extends != "layouts.admin" {
+ t.Errorf("Extends = %q, want %q", tpl.Extends, "layouts.admin")
+ }
+ if len(tpl.Sections) != 2 {
+ t.Errorf("expected 2 sections, got %d", len(tpl.Sections))
+ }
+ if len(tpl.Includes) != 3 {
+ t.Errorf("expected 3 includes (2 include + 1 component), got %d", len(tpl.Includes))
+ }
+ if len(tpl.Stacks) != 2 {
+ t.Errorf("expected 2 stacks (scripts, styles), got %d", len(tpl.Stacks))
+ }
+}
+
+func TestBladeViewName(t *testing.T) {
+ tests := []struct {
+ input string
+ want string
+ }{
+ {"/project/resources/views/layouts/app.blade.php", "layouts.app"},
+ {"/project/resources/views/pages/home.blade.php", "pages.home"},
+ {"/project/resources/views/welcome.blade.php", "welcome"},
+ {"/random/path/file.blade.php", "file"},
+ }
+ for _, tt := range tests {
+ got := bladeViewName(tt.input)
+ if got != tt.want {
+ t.Errorf("bladeViewName(%q) = %q, want %q", tt.input, got, tt.want)
+ }
+ }
+}
diff --git a/pkg/parser/php/laravel/enricher.go b/pkg/parser/php/laravel/enricher.go
index 10e1c4f..bad867d 100644
--- a/pkg/parser/php/laravel/enricher.go
+++ b/pkg/parser/php/laravel/enricher.go
@@ -123,6 +123,21 @@ func (e *Enricher) Enrich(ca *php.CodeAnalyzer, packages []*php.PackageInfo, pat
}
}
+ logger.Instance.Debug("[LARAVEL] Enrich: %d chunks after routes, before blade analysis", len(chunks))
+
+ // Analyze Blade Templates
+ bladeFiles := e.adapter.findBladeFiles(paths)
+ logger.Instance.Debug("[LARAVEL] Enrich: found %d blade files from paths=%v", len(bladeFiles), paths)
+ if len(bladeFiles) > 0 {
+ bladeAnalyzer := NewBladeAnalyzer()
+ bladeTemplates := bladeAnalyzer.Analyze(bladeFiles)
+ if len(bladeTemplates) > 0 {
+ bladeChunks := e.adapter.convertBladeToChunks(bladeTemplates)
+ logger.Instance.Debug("[LARAVEL] Enrich: %d blade templates → %d chunks", len(bladeTemplates), len(bladeChunks))
+ chunks = append(chunks, bladeChunks...)
+ }
+ }
+
logger.Instance.Debug("[LARAVEL] Enrich DONE: returning %d total chunks", len(chunks))
return chunks
}
diff --git a/pkg/parser/php/laravel/types.go b/pkg/parser/php/laravel/types.go
index cf17c1e..83e2602 100644
--- a/pkg/parser/php/laravel/types.go
+++ b/pkg/parser/php/laravel/types.go
@@ -2,11 +2,12 @@ package laravel
// LaravelInfo contains Laravel-specific framework information extracted from a project
type LaravelInfo struct {
- Models []EloquentModel `json:"models"`
- Controllers []Controller `json:"controllers"`
- Routes []Route `json:"routes"`
- Migrations []Migration `json:"migrations,omitempty"`
- Middleware []Middleware `json:"middleware,omitempty"`
+ Models []EloquentModel `json:"models"`
+ Controllers []Controller `json:"controllers"`
+ Routes []Route `json:"routes"`
+ Migrations []Migration `json:"migrations,omitempty"`
+ Middleware []Middleware `json:"middleware,omitempty"`
+ BladeTemplates []BladeTemplate `json:"blade_templates,omitempty"`
}
// EloquentModel represents a Laravel Eloquent model with ORM features
@@ -128,3 +129,29 @@ type Middleware struct {
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
}
+
+// BladeTemplate represents a parsed Blade template with its directives
+type BladeTemplate struct {
+ Name string `json:"name"` // dot notation: layouts.app
+ FilePath string `json:"file_path"`
+ TotalLines int `json:"total_lines"` // total line count of the file
+ Extends string `json:"extends,omitempty"` // @extends('...')
+ Sections []BladeSection `json:"sections,omitempty"`
+ Includes []BladeInclude `json:"includes,omitempty"`
+ Stacks []string `json:"stacks,omitempty"` // @push/@stack names
+ Props []string `json:"props,omitempty"` // @props([...])
+}
+
+// BladeSection represents a @section or @yield directive
+type BladeSection struct {
+ Name string `json:"name"` // section name
+ Type string `json:"type"` // "section" or "yield"
+ StartLine int `json:"start_line"`
+}
+
+// BladeInclude represents an @include, @component, or @each directive
+type BladeInclude struct {
+ ViewName string `json:"view_name"` // dot notation
+ Type string `json:"type"` // "include", "component", or "each"
+ Line int `json:"line"`
+}
diff --git a/pkg/parser/php/wordpress/analyzer.go b/pkg/parser/php/wordpress/analyzer.go
index f22af76..917ab15 100644
--- a/pkg/parser/php/wordpress/analyzer.go
+++ b/pkg/parser/php/wordpress/analyzer.go
@@ -15,6 +15,8 @@ import (
pkgParser "github.com/doITmagic/rag-code-mcp/pkg/parser"
"github.com/doITmagic/rag-code-mcp/pkg/parser/php"
+ "github.com/doITmagic/rag-code-mcp/pkg/parser/php/wordpress/oxygen"
+ "github.com/doITmagic/rag-code-mcp/pkg/parser/php/wordpress/woocommerce"
)
// Analyzer is the main WordPress framework analyzer that coordinates all WordPress-specific analyzers
@@ -26,6 +28,8 @@ type Analyzer struct {
widgetAnalyzer *WidgetAnalyzer
adminAnalyzer *AdminAnalyzer
pluginHeaderAnalyzer *PluginHeaderAnalyzer
+ oxygenAnalyzer *oxygen.Analyzer
+ woocommerceAnalyzer *woocommerce.Analyzer
phpAnalyzer *php.CodeAnalyzer
}
@@ -39,6 +43,8 @@ func NewAnalyzer() *Analyzer {
widgetAnalyzer: NewWidgetAnalyzer(),
adminAnalyzer: NewAdminAnalyzer(),
pluginHeaderAnalyzer: NewPluginHeaderAnalyzer(),
+ oxygenAnalyzer: oxygen.NewAnalyzer(),
+ woocommerceAnalyzer: woocommerce.NewAnalyzer(),
phpAnalyzer: php.NewCodeAnalyzer(),
}
}
@@ -74,6 +80,7 @@ func (a *Analyzer) AnalyzePaths(paths []string) ([]php.CodeChunk, error) {
// analyzeWordPress performs complete WordPress analysis using both package info and AST
func (a *Analyzer) analyzeWordPress(packages []*php.PackageInfo, paths []string) *WordPressInfo {
info := &WordPressInfo{}
+ var wcAPICalls []woocommerce.WCAPICall
// Analyze from parsed package info (method calls)
info.Hooks = a.hookAnalyzer.AnalyzeHooks(packages)
@@ -143,10 +150,44 @@ func (a *Analyzer) analyzeWordPress(packages []*php.PackageInfo, paths []string)
}
}
+ // WooCommerce API calls from AST
+ wcInfo := a.woocommerceAnalyzer.Analyze(rootNode, path)
+ if wcInfo != nil && len(wcInfo.APICalls) > 0 {
+ wcAPICalls = append(wcAPICalls, wcInfo.APICalls...)
+ }
+
return nil
})
}
+ // Oxygen Builder analysis (reuses already-parsed packages)
+ oxyInfo := a.oxygenAnalyzer.AnalyzeFromPackages(packages)
+ if oxyInfo != nil && (len(oxyInfo.Elements) > 0 || len(oxyInfo.Templates) > 0) {
+ info.OxygenInfo = oxyInfo
+ }
+
+ // WooCommerce analysis: classify WP hooks that have woocommerce_ prefix
+ var wcInputHooks []woocommerce.WPHookInput
+ for _, h := range info.Hooks {
+ wcInputHooks = append(wcInputHooks, woocommerce.WPHookInput{
+ Type: string(h.Type),
+ Name: h.Name,
+ Callback: h.Callback,
+ Priority: h.Priority,
+ FilePath: h.FilePath,
+ StartLine: h.StartLine,
+ EndLine: h.EndLine,
+ })
+ }
+ wcHooks := a.woocommerceAnalyzer.AnalyzeHooksFromWP(wcInputHooks)
+ if len(wcHooks) > 0 || len(wcAPICalls) > 0 {
+ wcInfo := &woocommerce.WooCommerceInfo{
+ Hooks: wcHooks,
+ APICalls: wcAPICalls,
+ }
+ info.WooCommerceInfo = wcInfo
+ }
+
return info
}
@@ -344,6 +385,96 @@ func (a *Analyzer) convertToChunks(info *WordPressInfo) []php.CodeChunk {
})
}
+ // Convert Oxygen elements
+ if info.OxygenInfo != nil {
+ if oxyInfo, ok := info.OxygenInfo.(*oxygen.OxygenInfo); ok {
+ for _, elem := range oxyInfo.Elements {
+ baseClass := elem.BaseClass
+ if baseClass == "" {
+ baseClass = "OxyEl"
+ }
+ chunks = append(chunks, php.CodeChunk{
+ Name: elem.ClassName,
+ Type: "oxy_element",
+ Language: "php",
+ FilePath: elem.FilePath,
+ StartLine: elem.StartLine,
+ EndLine: elem.EndLine,
+ Signature: fmt.Sprintf("class %s extends %s", elem.ClassName, baseClass),
+ Docstring: fmt.Sprintf("Oxygen Builder Element: %s (methods: %s)", elem.ClassName, strings.Join(elem.Methods, ", ")),
+ Metadata: map[string]any{
+ "framework": "wordpress",
+ "wp_type": "oxygen_element",
+ "namespace": elem.Namespace,
+ "has_slug": elem.SlugMethod,
+ "methods": elem.Methods,
+ },
+ })
+ }
+
+ for _, tmpl := range oxyInfo.Templates {
+ chunks = append(chunks, php.CodeChunk{
+ Name: tmpl.PostType,
+ Type: "oxy_template",
+ Language: "php",
+ FilePath: tmpl.FilePath,
+ StartLine: tmpl.Line,
+ EndLine: tmpl.Line,
+ Signature: fmt.Sprintf("register_post_type('%s', ...)", tmpl.PostType),
+ Docstring: fmt.Sprintf("Oxygen Template: %s", tmpl.PostType),
+ Metadata: map[string]any{
+ "framework": "wordpress",
+ "wp_type": "oxygen_template",
+ },
+ })
+ }
+ }
+ }
+
+ // Convert WooCommerce hooks
+ if info.WooCommerceInfo != nil {
+ if wcInfo, ok := info.WooCommerceInfo.(*woocommerce.WooCommerceInfo); ok {
+ for _, wcHook := range wcInfo.Hooks {
+ chunks = append(chunks, php.CodeChunk{
+ Name: wcHook.HookName,
+ Type: "wc_hook",
+ Language: "php",
+ FilePath: wcHook.FilePath,
+ StartLine: wcHook.StartLine,
+ EndLine: wcHook.EndLine,
+ Signature: buildWCHookSignature(wcHook),
+ Docstring: fmt.Sprintf("WooCommerce %s hook (%s area): %s", wcHook.HookType, wcHook.Area, wcHook.HookName),
+ Metadata: map[string]any{
+ "framework": "wordpress",
+ "wp_type": "wc_hook",
+ "wc_area": string(wcHook.Area),
+ "hook_type": wcHook.HookType,
+ "callback": wcHook.Callback,
+ "priority": wcHook.Priority,
+ },
+ })
+ }
+
+ for _, apiCall := range wcInfo.APICalls {
+ chunks = append(chunks, php.CodeChunk{
+ Name: apiCall.Function,
+ Type: "wc_api_call",
+ Language: "php",
+ FilePath: apiCall.FilePath,
+ StartLine: apiCall.StartLine,
+ EndLine: apiCall.EndLine,
+ Signature: fmt.Sprintf("%s(...)", apiCall.Function),
+ Docstring: fmt.Sprintf("WooCommerce API call: %s (category: %s)", apiCall.Function, apiCall.Category),
+ Metadata: map[string]any{
+ "framework": "wordpress",
+ "wp_type": "wc_api_call",
+ "wc_category": apiCall.Category,
+ },
+ })
+ }
+ }
+ }
+
return chunks
}
@@ -387,6 +518,37 @@ func buildHookSignature(hook WPHook) string {
}
}
+// buildWCHookSignature creates a readable signature for a WooCommerce hook
+// using the real WordPress function names instead of raw hook type values
+func buildWCHookSignature(wcHook woocommerce.WCHook) string {
+ switch wcHook.HookType {
+ case "action":
+ if wcHook.Priority > 0 {
+ return fmt.Sprintf("add_action('%s', '%s', %d)", wcHook.HookName, wcHook.Callback, wcHook.Priority)
+ }
+ return fmt.Sprintf("add_action('%s', '%s')", wcHook.HookName, wcHook.Callback)
+ case "filter":
+ if wcHook.Priority > 0 {
+ return fmt.Sprintf("add_filter('%s', '%s', %d)", wcHook.HookName, wcHook.Callback, wcHook.Priority)
+ }
+ return fmt.Sprintf("add_filter('%s', '%s')", wcHook.HookName, wcHook.Callback)
+ case "action_trigger":
+ return fmt.Sprintf("do_action('%s')", wcHook.HookName)
+ case "filter_trigger":
+ return fmt.Sprintf("apply_filters('%s')", wcHook.HookName)
+ case "action_removal":
+ return fmt.Sprintf("remove_action('%s', '%s')", wcHook.HookName, wcHook.Callback)
+ case "filter_removal":
+ return fmt.Sprintf("remove_filter('%s', '%s')", wcHook.HookName, wcHook.Callback)
+ case "action_check":
+ return fmt.Sprintf("has_action('%s')", wcHook.HookName)
+ case "filter_check":
+ return fmt.Sprintf("has_filter('%s')", wcHook.HookName)
+ default:
+ return fmt.Sprintf("%s('%s', '%s')", wcHook.HookType, wcHook.HookName, wcHook.Callback)
+ }
+}
+
// IsWordPressProject detects if the given paths contain a WordPress project.
// It first checks the paths directly (directory-level indicators, plugin headers),
// then walks UP parent directories to find WordPress root indicators.
diff --git a/pkg/parser/php/wordpress/analyzer_test.go b/pkg/parser/php/wordpress/analyzer_test.go
index 8bb7d87..82226b7 100644
--- a/pkg/parser/php/wordpress/analyzer_test.go
+++ b/pkg/parser/php/wordpress/analyzer_test.go
@@ -385,3 +385,138 @@ func TestMergePostTypes_Deduplication(t *testing.T) {
t.Errorf("expected 2 post types after merge, got %d", len(result))
}
}
+
+func TestAnalyzer_OxygenElementDetection(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ // Plugin header to detect as WordPress
+ err := os.WriteFile(filepath.Join(tmpDir, "plugin.php"), []byte(`Custom';
+ }
+}
+`), 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ analyzer := NewAnalyzer()
+ chunks, err := analyzer.AnalyzePaths([]string{tmpDir})
+ if err != nil {
+ t.Fatalf("AnalyzePaths failed: %v", err)
+ }
+
+ // Find oxy_element chunks
+ var oxyChunks []string
+ for _, c := range chunks {
+ if c.Type == "oxy_element" {
+ oxyChunks = append(oxyChunks, c.Name)
+ if c.Metadata["framework"] != "wordpress" {
+ t.Errorf("Oxygen chunk %s: expected framework=wordpress", c.Name)
+ }
+ if c.Metadata["wp_type"] != "oxygen_element" {
+ t.Errorf("Oxygen chunk %s: expected wp_type=oxygen_element, got %v", c.Name, c.Metadata["wp_type"])
+ }
+ }
+ }
+
+ if len(oxyChunks) == 0 {
+ t.Error("expected at least 1 oxy_element chunk, got 0")
+ } else {
+ t.Logf("Found Oxygen elements: %v", oxyChunks)
+ }
+}
+
+func TestAnalyzer_WooCommerceHookClassification(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ // Plugin header
+ err := os.WriteFile(filepath.Join(tmpDir, "plugin.php"), []byte(`= 0 {
+ return extends[idx+1:]
+ }
+ return extends
+}
+
// extractFuncName extracts function name from AST node
func extractFuncName(node ast.Vertex) string {
if node == nil {
diff --git a/pkg/parser/php/wordpress/oxygen/types.go b/pkg/parser/php/wordpress/oxygen/types.go
index dfe6c1e..8366e1a 100644
--- a/pkg/parser/php/wordpress/oxygen/types.go
+++ b/pkg/parser/php/wordpress/oxygen/types.go
@@ -12,8 +12,9 @@ type OxygenElement struct {
ClassName string `json:"class_name"`
Namespace string `json:"namespace,omitempty"`
FullName string `json:"full_name"`
- SlugMethod bool `json:"has_slug"` // Has slug() method
- Methods []string `json:"methods,omitempty"` // Detected methods (init, name, slug, icon, controls, render)
+ BaseClass string `json:"base_class,omitempty"` // OxyEl, OxyElShadow, OxygenElement, etc.
+ SlugMethod bool `json:"has_slug"` // Has slug() method
+ Methods []string `json:"methods,omitempty"` // Detected methods (init, name, slug, icon, controls, render)
FilePath string `json:"file_path"`
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
diff --git a/pkg/parser/php/wordpress/types.go b/pkg/parser/php/wordpress/types.go
index ace002a..397c1de 100644
--- a/pkg/parser/php/wordpress/types.go
+++ b/pkg/parser/php/wordpress/types.go
@@ -2,16 +2,18 @@ package wordpress
// WordPressInfo contains WordPress-specific framework information extracted from a project
type WordPressInfo struct {
- Hooks []WPHook `json:"hooks,omitempty"`
- PostTypes []PostType `json:"post_types,omitempty"`
- Taxonomies []Taxonomy `json:"taxonomies,omitempty"`
- Shortcodes []Shortcode `json:"shortcodes,omitempty"`
- Blocks []Block `json:"blocks,omitempty"`
- BlockPatterns []BlockPattern `json:"block_patterns,omitempty"`
- Widgets []Widget `json:"widgets,omitempty"`
- AdminPages []AdminPage `json:"admin_pages,omitempty"`
- Settings []Setting `json:"settings,omitempty"`
- PluginHeader *PluginHeader `json:"plugin_header,omitempty"`
+ Hooks []WPHook `json:"hooks,omitempty"`
+ PostTypes []PostType `json:"post_types,omitempty"`
+ Taxonomies []Taxonomy `json:"taxonomies,omitempty"`
+ Shortcodes []Shortcode `json:"shortcodes,omitempty"`
+ Blocks []Block `json:"blocks,omitempty"`
+ BlockPatterns []BlockPattern `json:"block_patterns,omitempty"`
+ Widgets []Widget `json:"widgets,omitempty"`
+ AdminPages []AdminPage `json:"admin_pages,omitempty"`
+ Settings []Setting `json:"settings,omitempty"`
+ PluginHeader *PluginHeader `json:"plugin_header,omitempty"`
+ OxygenInfo any `json:"oxygen,omitempty"` // *oxygen.OxygenInfo (avoid import cycle)
+ WooCommerceInfo any `json:"woocommerce,omitempty"` // *woocommerce.WooCommerceInfo (avoid import cycle)
}
// HookType represents the type of a WordPress hook
diff --git a/pkg/parser/php/wordpress/woocommerce/analyzer.go b/pkg/parser/php/wordpress/woocommerce/analyzer.go
index bc1273a..85056a3 100644
--- a/pkg/parser/php/wordpress/woocommerce/analyzer.go
+++ b/pkg/parser/php/wordpress/woocommerce/analyzer.go
@@ -1,12 +1,24 @@
package woocommerce
import (
+ "strconv"
"strings"
"github.com/VKCOM/php-parser/pkg/ast"
- "github.com/doITmagic/rag-code-mcp/pkg/parser/php/wordpress"
)
+// WPHookInput is a local mirror of wordpress.WPHook to avoid import cycles.
+// The wordpress/analyzer.go converts WPHook to WPHookInput before calling AnalyzeHooksFromWP.
+type WPHookInput struct {
+ Type string
+ Name string
+ Callback string
+ Priority int
+ FilePath string
+ StartLine int
+ EndLine int
+}
+
// areaRule maps a keyword to a WC functional area
type areaRule struct {
keyword string
@@ -55,15 +67,11 @@ var wcApiFunctions = map[string]string{
}
// Analyzer detects WooCommerce-specific patterns in WordPress code
-type Analyzer struct {
- astHelper *wordpress.ASTHelper
-}
+type Analyzer struct{}
// NewAnalyzer creates a new WooCommerce analyzer
func NewAnalyzer() *Analyzer {
- return &Analyzer{
- astHelper: wordpress.NewASTHelper(),
- }
+ return &Analyzer{}
}
// Analyze performs WooCommerce-specific analysis on AST
@@ -73,9 +81,9 @@ func (a *Analyzer) Analyze(root ast.Vertex, filePath string) *WooCommerceInfo {
return info
}
-// AnalyzeHooksFromWP classifies existing WordPress hooks as WooCommerce hooks
-// This takes already-detected WP hooks and enriches them with WC area info
-func (a *Analyzer) AnalyzeHooksFromWP(wpHooks []wordpress.WPHook) []WCHook {
+// AnalyzeHooksFromWP classifies existing WordPress hooks as WooCommerce hooks.
+// Uses WPHookInput to avoid import cycle with the wordpress parent package.
+func (a *Analyzer) AnalyzeHooksFromWP(wpHooks []WPHookInput) []WCHook {
var wcHooks []WCHook
for _, h := range wpHooks {
@@ -86,7 +94,7 @@ func (a *Analyzer) AnalyzeHooksFromWP(wpHooks []wordpress.WPHook) []WCHook {
wcHook := WCHook{
HookName: h.Name,
Area: classifyHookArea(h.Name),
- HookType: string(h.Type),
+ HookType: h.Type,
Callback: h.Callback,
Priority: h.Priority,
FilePath: h.FilePath,
@@ -160,23 +168,13 @@ func (a *Analyzer) walk(node ast.Vertex, filePath string, info *WooCommerceInfo)
a.walk(n.Expr, filePath, info)
case *ast.ExprFunctionCall:
// Check for WC hooks
- hook := a.astHelper.ExtractHookFromFunctionCall(n, filePath)
- if hook != nil && strings.HasPrefix(hook.Name, "woocommerce_") {
- wcHook := WCHook{
- HookName: hook.Name,
- Area: classifyHookArea(hook.Name),
- HookType: string(hook.Type),
- Callback: hook.Callback,
- Priority: hook.Priority,
- FilePath: hook.FilePath,
- StartLine: hook.StartLine,
- EndLine: hook.EndLine,
- }
- info.Hooks = append(info.Hooks, wcHook)
+ hook := extractHookFromFunctionCall(n, filePath)
+ if hook != nil && strings.HasPrefix(hook.HookName, "woocommerce_") {
+ info.Hooks = append(info.Hooks, *hook)
}
// Check for WC API calls
- funcName := a.extractFuncName(n.Function)
+ funcName := extractFuncName(n.Function)
if category, ok := wcApiFunctions[funcName]; ok {
info.APICalls = append(info.APICalls, WCAPICall{
Function: funcName,
@@ -189,10 +187,10 @@ func (a *Analyzer) walk(node ast.Vertex, filePath string, info *WooCommerceInfo)
case *ast.ExprStaticCall:
// Check for WC()->... static calls
- className := a.extractClassName(n.Class)
+ className := extractClassName(n.Class)
if className == "WC" {
info.APICalls = append(info.APICalls, WCAPICall{
- Function: "WC::" + a.extractMethodName(n.Call),
+ Function: "WC::" + extractMethodName(n.Call),
Category: "core",
FilePath: filePath,
StartLine: n.Position.StartLine,
@@ -216,8 +214,67 @@ func classifyHookArea(hookName string) WCHookArea {
return WCAreaGeneral
}
+// --- Local AST helpers (avoid importing wordpress parent package) ---
+
+// hookFuncMap maps WordPress hook function names to their HookType string
+var hookFuncMap = map[string]string{
+ "add_action": "action",
+ "add_filter": "filter",
+ "do_action": "action_trigger",
+ "apply_filters": "filter_trigger",
+ "remove_action": "action_removal",
+ "remove_filter": "filter_removal",
+ "has_filter": "filter_check",
+ "has_action": "action_check",
+}
+
+// extractHookFromFunctionCall checks if a function call is a WordPress hook and returns a WCHook if it's WC-related
+func extractHookFromFunctionCall(call *ast.ExprFunctionCall, filePath string) *WCHook {
+ funcName := extractFuncName(call.Function)
+ if funcName == "" {
+ return nil
+ }
+
+ hookType, ok := hookFuncMap[funcName]
+ if !ok {
+ return nil
+ }
+
+ // Extract arguments
+ args := extractCallArgs(call.Args)
+ if len(args) == 0 {
+ return nil
+ }
+
+ hookName := args[0]
+
+ hook := &WCHook{
+ HookName: hookName,
+ Area: classifyHookArea(hookName),
+ HookType: hookType,
+ FilePath: filePath,
+ StartLine: call.Position.StartLine,
+ EndLine: call.Position.EndLine,
+ }
+
+ // For add/remove hooks: callback, priority
+ if hookType == "action" || hookType == "filter" ||
+ hookType == "action_removal" || hookType == "filter_removal" {
+ if len(args) > 1 {
+ hook.Callback = args[1]
+ }
+ if len(args) > 2 {
+ if p, err := strconv.Atoi(args[2]); err == nil {
+ hook.Priority = p
+ }
+ }
+ }
+
+ return hook
+}
+
// extractFuncName extracts function name from AST node
-func (a *Analyzer) extractFuncName(node ast.Vertex) string {
+func extractFuncName(node ast.Vertex) string {
if node == nil {
return ""
}
@@ -230,17 +287,25 @@ func (a *Analyzer) extractFuncName(node ast.Vertex) string {
}
}
return strings.Join(parts, "\\")
+ case *ast.NameFullyQualified:
+ var parts []string
+ for _, part := range n.Parts {
+ if namePart, ok := part.(*ast.NamePart); ok {
+ parts = append(parts, string(namePart.Value))
+ }
+ }
+ return strings.Join(parts, "\\")
}
return ""
}
// extractClassName extracts class name from AST node
-func (a *Analyzer) extractClassName(node ast.Vertex) string {
- return a.extractFuncName(node)
+func extractClassName(node ast.Vertex) string {
+ return extractFuncName(node)
}
// extractMethodName extracts method name from AST node
-func (a *Analyzer) extractMethodName(node ast.Vertex) string {
+func extractMethodName(node ast.Vertex) string {
if node == nil {
return ""
}
@@ -249,3 +314,78 @@ func (a *Analyzer) extractMethodName(node ast.Vertex) string {
}
return ""
}
+
+// extractCallArgs extracts string arguments from a function call
+func extractCallArgs(args []ast.Vertex) []string {
+ var result []string
+ for _, arg := range args {
+ if argNode, ok := arg.(*ast.Argument); ok {
+ val := extractExprValue(argNode.Expr)
+ result = append(result, val)
+ }
+ }
+ return result
+}
+
+// extractExprValue extracts a string representation from an expression
+func extractExprValue(expr ast.Vertex) string {
+ if expr == nil {
+ return ""
+ }
+ switch n := expr.(type) {
+ case *ast.ScalarString:
+ val := string(n.Value)
+ if len(val) >= 2 {
+ val = val[1 : len(val)-1] // Remove quotes
+ }
+ return val
+ case *ast.ScalarLnumber:
+ return string(n.Value)
+ case *ast.ScalarDnumber:
+ return string(n.Value)
+ case *ast.Name:
+ var parts []string
+ for _, part := range n.Parts {
+ if namePart, ok := part.(*ast.NamePart); ok {
+ parts = append(parts, string(namePart.Value))
+ }
+ }
+ return strings.Join(parts, "\\")
+ case *ast.ExprConstFetch:
+ return extractExprValue(n.Const)
+ case *ast.ExprClassConstFetch:
+ if constName, ok := n.Const.(*ast.Identifier); ok {
+ if string(constName.Value) == "class" {
+ return extractExprValue(n.Class) + "::class"
+ }
+ }
+ case *ast.ExprVariable:
+ if nameNode, ok := n.Name.(*ast.Identifier); ok {
+ name := string(nameNode.Value)
+ if strings.HasPrefix(name, "$") {
+ return name
+ }
+ return "$" + name
+ }
+ case *ast.ExprArray:
+ if len(n.Items) == 2 {
+ items := make([]string, 0, 2)
+ for _, item := range n.Items {
+ if arrayItem, ok := item.(*ast.ExprArrayItem); ok {
+ items = append(items, extractExprValue(arrayItem.Val))
+ }
+ }
+ if len(items) == 2 {
+ return items[0] + "::" + items[1]
+ }
+ }
+ return "[array]"
+ case *ast.ExprClosure:
+ return "[closure]"
+ case *ast.ExprArrowFunction:
+ return "[arrow_fn]"
+ case *ast.Identifier:
+ return string(n.Value)
+ }
+ return ""
+}
diff --git a/pkg/parser/php/wordpress/woocommerce/analyzer_test.go b/pkg/parser/php/wordpress/woocommerce/analyzer_test.go
index ee0f668..e16b388 100644
--- a/pkg/parser/php/wordpress/woocommerce/analyzer_test.go
+++ b/pkg/parser/php/wordpress/woocommerce/analyzer_test.go
@@ -8,8 +8,6 @@ import (
"github.com/VKCOM/php-parser/pkg/errors"
"github.com/VKCOM/php-parser/pkg/parser"
"github.com/VKCOM/php-parser/pkg/version"
-
- "github.com/doITmagic/rag-code-mcp/pkg/parser/php/wordpress"
)
func parsePHP(t *testing.T, code string) ast.Vertex {
@@ -167,12 +165,12 @@ class MyWCPlugin {
}
func TestAnalyzeHooksFromWP(t *testing.T) {
- wpHooks := []wordpress.WPHook{
- {Type: wordpress.HookAction, Name: "init", Callback: "my_init", FilePath: "test.php", StartLine: 1},
- {Type: wordpress.HookAction, Name: "woocommerce_before_cart", Callback: "cart_fn", FilePath: "test.php", StartLine: 2, Priority: 20},
- {Type: wordpress.HookFilter, Name: "woocommerce_product_get_price", Callback: "price_fn", FilePath: "test.php", StartLine: 3},
- {Type: wordpress.HookAction, Name: "wp_enqueue_scripts", Callback: "enqueue", FilePath: "test.php", StartLine: 4},
- {Type: wordpress.HookActionTrigger, Name: "woocommerce_checkout_process", FilePath: "test.php", StartLine: 5},
+ wpHooks := []WPHookInput{
+ {Type: "action", Name: "init", Callback: "my_init", FilePath: "test.php", StartLine: 1},
+ {Type: "action", Name: "woocommerce_before_cart", Callback: "cart_fn", FilePath: "test.php", StartLine: 2, Priority: 20},
+ {Type: "filter", Name: "woocommerce_product_get_price", Callback: "price_fn", FilePath: "test.php", StartLine: 3},
+ {Type: "action", Name: "wp_enqueue_scripts", Callback: "enqueue", FilePath: "test.php", StartLine: 4},
+ {Type: "action_trigger", Name: "woocommerce_checkout_process", FilePath: "test.php", StartLine: 5},
}
analyzer := NewAnalyzer()