Skip to content

Commit 4dbdb7d

Browse files
author
razvan
committed
feat(php): add filesystem walk-up detection for Laravel & WordPress + structured logging
- Laravel: walk UP parent directories looking for artisan / composer.json with laravel/framework - WordPress: walk UP parent directories looking for wp-config.php, wp-content, plugin/theme headers - Add detection caching (laravelCache, wordpressCache) to avoid repeated stat calls - Replace all fmt.Fprintf(os.Stderr) with logger.Instance.{Debug,Info,Warn,Error} - Add detailed enrichment pipeline logging (chunks before/after, route files, etc.) - Bump version to 2.1.86
1 parent 90419b7 commit 4dbdb7d

8 files changed

Lines changed: 247 additions & 24 deletions

File tree

cmd/rag-code-mcp/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
)
1717

1818
var (
19-
Version = "2.1.80"
19+
Version = "2.1.86"
2020
Commit = "none"
2121
Date = "24.10.2025"
2222
)

pkg/parser/php/analyzer.go

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,26 @@ import (
1515
"github.com/VKCOM/php-parser/pkg/visitor"
1616
"github.com/VKCOM/php-parser/pkg/visitor/traverser"
1717

18+
"github.com/doITmagic/rag-code-mcp/internal/logger"
1819
pkgParser "github.com/doITmagic/rag-code-mcp/pkg/parser"
1920
)
2021

2122
// CodeAnalyzer implements PathAnalyzer for PHP
2223
type CodeAnalyzer struct {
2324
currentNamespace string
2425
packages map[string]*PackageInfo
26+
27+
// Cached framework detection results (per directory → result)
28+
laravelCache map[string]bool
29+
wordpressCache map[string]bool
2530
}
2631

2732
// NewCodeAnalyzer creates a new PHP code analyzer
2833
func NewCodeAnalyzer() *CodeAnalyzer {
2934
return &CodeAnalyzer{
30-
packages: make(map[string]*PackageInfo),
35+
packages: make(map[string]*PackageInfo),
36+
laravelCache: make(map[string]bool),
37+
wordpressCache: make(map[string]bool),
3138
}
3239
}
3340

@@ -79,12 +86,12 @@ func (ca *CodeAnalyzer) AnalyzePaths(paths []string) ([]CodeChunk, error) {
7986

8087
content, err := os.ReadFile(path)
8188
if err != nil {
82-
fmt.Fprintf(os.Stderr, "Warning: failed to read %s: %v\n", path, err)
89+
logger.Instance.Warn("[PHP] failed to read %s: %v", path, err)
8390
return nil
8491
}
8592

8693
if err := ca.parseAndCollect(path, content); err != nil {
87-
fmt.Fprintf(os.Stderr, "Warning: failed to analyze %s: %v\n", path, err)
94+
logger.Instance.Warn("[PHP] failed to analyze %s: %v", path, err)
8895
}
8996
return nil
9097
})
@@ -136,13 +143,13 @@ func (ca *CodeAnalyzer) parseAndCollect(filePath string, content []byte) error {
136143
if len(parserErrors) > 0 {
137144
// Only log first few errors to avoid spam
138145
maxErrors := 3
139-
fmt.Fprintf(os.Stderr, "PHP parser warnings in %s:\n", filePath)
146+
logger.Instance.Debug("[PHP] parser warnings in %s:", filePath)
140147
for i, e := range parserErrors {
141148
if i >= maxErrors {
142-
fmt.Fprintf(os.Stderr, " ... and %d more\n", len(parserErrors)-maxErrors)
149+
logger.Instance.Debug("[PHP] ... and %d more", len(parserErrors)-maxErrors)
143150
break
144151
}
145-
fmt.Fprintf(os.Stderr, " %s\n", e.String())
152+
logger.Instance.Debug("[PHP] %s", e.String())
146153
}
147154
}
148155

@@ -1198,15 +1205,13 @@ func (v *symbolCollector) walkExpr(expr ast.Vertex, calls *[]MethodCall) {
11981205

11991206
// IsLaravelProject detects if the analyzed code is from a Laravel project
12001207
func (ca *CodeAnalyzer) IsLaravelProject() bool {
1208+
// 1. Quick check: namespace/class-based detection from parsed packages
12011209
for _, pkg := range ca.packages {
1202-
// Check for Laravel-specific namespaces
12031210
if strings.HasPrefix(pkg.Namespace, "App\\Models") ||
12041211
strings.HasPrefix(pkg.Namespace, "App\\Http\\Controllers") ||
12051212
strings.HasPrefix(pkg.Namespace, "Illuminate\\") {
12061213
return true
12071214
}
1208-
1209-
// Check for Laravel base classes
12101215
for _, class := range pkg.Classes {
12111216
if class.Extends == "Model" ||
12121217
class.Extends == "Controller" ||
@@ -1216,6 +1221,58 @@ func (ca *CodeAnalyzer) IsLaravelProject() bool {
12161221
}
12171222
}
12181223
}
1224+
1225+
// 2. Filesystem walk-up: check for "artisan" file by walking parent dirs
1226+
for _, pkg := range ca.packages {
1227+
for _, class := range pkg.Classes {
1228+
if class.FilePath != "" {
1229+
if ca.isLaravelByFilesystem(class.FilePath) {
1230+
return true
1231+
}
1232+
}
1233+
}
1234+
for _, fn := range pkg.Functions {
1235+
if fn.FilePath != "" {
1236+
if ca.isLaravelByFilesystem(fn.FilePath) {
1237+
return true
1238+
}
1239+
}
1240+
}
1241+
}
1242+
return false
1243+
}
1244+
1245+
// isLaravelByFilesystem walks up from filePath checking for Laravel root indicators.
1246+
// Results are cached per directory to avoid repeated stat calls.
1247+
func (ca *CodeAnalyzer) isLaravelByFilesystem(filePath string) bool {
1248+
dir := filepath.Dir(filePath)
1249+
for {
1250+
if result, ok := ca.laravelCache[dir]; ok {
1251+
return result
1252+
}
1253+
1254+
// Check for artisan (the strongest Laravel indicator)
1255+
if _, err := os.Stat(filepath.Join(dir, "artisan")); err == nil {
1256+
ca.laravelCache[dir] = true
1257+
return true
1258+
}
1259+
1260+
// Also check for composer.json with laravel/framework
1261+
composerPath := filepath.Join(dir, "composer.json")
1262+
if content, err := os.ReadFile(composerPath); err == nil {
1263+
if strings.Contains(string(content), "laravel/framework") {
1264+
ca.laravelCache[dir] = true
1265+
return true
1266+
}
1267+
}
1268+
1269+
parent := filepath.Dir(dir)
1270+
if parent == dir {
1271+
break // reached filesystem root
1272+
}
1273+
dir = parent
1274+
}
1275+
ca.laravelCache[dir] = false
12191276
return false
12201277
}
12211278

pkg/parser/php/laravel/enricher.go

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package laravel
22

33
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
8+
"github.com/doITmagic/rag-code-mcp/internal/logger"
49
"github.com/doITmagic/rag-code-mcp/pkg/parser/php"
510
)
611

@@ -15,32 +20,101 @@ func init() {
1520
})
1621
}
1722

18-
// IsApplicable checks if the parsed paths correspond to a Laravel project
23+
// IsApplicable checks if the parsed paths correspond to a Laravel project.
1924
func (e *Enricher) IsApplicable(ca *php.CodeAnalyzer, paths []string) bool {
20-
return ca.IsLaravelProject()
25+
// 1. Quick: namespace/class-based detection from already-parsed packages
26+
byPackages := ca.IsLaravelProject()
27+
logger.Instance.Debug("[LARAVEL] IsApplicable: ca.IsLaravelProject()=%v for paths=%v", byPackages, paths)
28+
if byPackages {
29+
return true
30+
}
31+
32+
// 2. Filesystem walk-up
33+
byFS := IsLaravelProjectByPaths(paths)
34+
logger.Instance.Debug("[LARAVEL] IsApplicable: IsLaravelProjectByPaths()=%v for paths=%v", byFS, paths)
35+
return byFS
36+
}
37+
38+
// IsLaravelProjectByPaths walks UP parent directories from the given paths
39+
// looking for Laravel root indicators (artisan file, composer.json with laravel/framework).
40+
func IsLaravelProjectByPaths(paths []string) bool {
41+
for _, p := range paths {
42+
dir := p
43+
info, err := os.Stat(p)
44+
if err != nil {
45+
logger.Instance.Debug("[LARAVEL] IsLaravelProjectByPaths: stat error for %s: %v", p, err)
46+
continue
47+
}
48+
if !info.IsDir() {
49+
dir = filepath.Dir(p)
50+
}
51+
52+
if isLaravelRoot(dir) {
53+
logger.Instance.Debug("[LARAVEL] IsLaravelProjectByPaths: FOUND Laravel root walking up from %s", dir)
54+
return true
55+
}
56+
logger.Instance.Debug("[LARAVEL] IsLaravelProjectByPaths: NO Laravel root found walking up from %s", dir)
57+
}
58+
return false
59+
}
60+
61+
// isLaravelRoot walks UP from dir checking each parent for Laravel indicators.
62+
func isLaravelRoot(dir string) bool {
63+
for {
64+
// Check for artisan (the strongest Laravel indicator)
65+
artisanPath := filepath.Join(dir, "artisan")
66+
if _, err := os.Stat(artisanPath); err == nil {
67+
logger.Instance.Debug("[LARAVEL] isLaravelRoot: FOUND artisan at %s", artisanPath)
68+
return true
69+
}
70+
71+
// Check for composer.json with laravel/framework
72+
composerPath := filepath.Join(dir, "composer.json")
73+
if content, err := os.ReadFile(composerPath); err == nil {
74+
if strings.Contains(string(content), "laravel/framework") {
75+
logger.Instance.Debug("[LARAVEL] isLaravelRoot: FOUND laravel/framework in %s", composerPath)
76+
return true
77+
}
78+
}
79+
80+
parent := filepath.Dir(dir)
81+
if parent == dir {
82+
break // reached filesystem root
83+
}
84+
dir = parent
85+
}
86+
return false
2187
}
2288

2389
// Enrich receives the base PHP chunks and analyzed packages and returns chunks merged with Laravel specifics
2490
func (e *Enricher) Enrich(ca *php.CodeAnalyzer, packages []*php.PackageInfo, paths []string, chunks []php.CodeChunk) []php.CodeChunk {
91+
logger.Instance.Info("[LARAVEL] Enrich: %d packages, %d paths, %d existing chunks", len(packages), len(paths), len(chunks))
92+
2593
// Run Laravel-specific package analysis for Controllers and Eloquent Models
2694
for _, pkg := range packages {
2795
analyzer := NewAnalyzer(pkg)
2896
info := analyzer.Analyze()
97+
logger.Instance.Info("[LARAVEL] Enrich pkg=%s: models=%d controllers=%d", pkg.Namespace, len(info.Models), len(info.Controllers))
2998

3099
// Enrich existing chunks with Laravel context (table, fillable, api routes)
31100
e.adapter.enrichChunks(chunks, info)
32101
}
33102

34-
// Analyze Routes (these are handled separately since they are mostly top level closures inside routes/)
103+
// Analyze Routes
35104
routeFiles := e.adapter.findRouteFiles(paths)
105+
logger.Instance.Info("[LARAVEL] Enrich: found %d route files from paths=%v", len(routeFiles), paths)
36106
if len(routeFiles) > 0 {
37107
routeAnalyzer := NewRouteAnalyzer()
38108
routes, err := routeAnalyzer.Analyze(routeFiles)
39109
if err == nil {
40110
routeChunks := e.adapter.convertRoutesToChunks(routes)
111+
logger.Instance.Info("[LARAVEL] Enrich: %d routes → %d chunks", len(routes), len(routeChunks))
41112
chunks = append(chunks, routeChunks...)
113+
} else {
114+
logger.Instance.Error("[LARAVEL] Enrich: route analysis error: %v", err)
42115
}
43116
}
44117

118+
logger.Instance.Info("[LARAVEL] Enrich DONE: returning %d total chunks", len(chunks))
45119
return chunks
46120
}

pkg/parser/php/laravel/migrations.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package laravel
22

33
import (
4-
"fmt"
54
"os"
65
"strings"
76

@@ -12,6 +11,8 @@ import (
1211
"github.com/VKCOM/php-parser/pkg/version"
1312
"github.com/VKCOM/php-parser/pkg/visitor"
1413
"github.com/VKCOM/php-parser/pkg/visitor/traverser"
14+
15+
"github.com/doITmagic/rag-code-mcp/internal/logger"
1516
)
1617

1718
// MigrationAnalyzer parses Laravel migration files
@@ -33,7 +34,7 @@ func (ma *MigrationAnalyzer) Analyze(filePaths []string) ([]Migration, error) {
3334
for _, path := range filePaths {
3435
migrations, err := ma.analyzeFile(path)
3536
if err != nil {
36-
fmt.Fprintf(os.Stderr, "Error analyzing migration file %s: %v\n", path, err)
37+
logger.Instance.Warn("[LARAVEL] Error analyzing migration file %s: %v", path, err)
3738
continue
3839
}
3940
allMigrations = append(allMigrations, migrations...)

pkg/parser/php/laravel/routes.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"github.com/VKCOM/php-parser/pkg/version"
1313
"github.com/VKCOM/php-parser/pkg/visitor"
1414
"github.com/VKCOM/php-parser/pkg/visitor/traverser"
15+
16+
"github.com/doITmagic/rag-code-mcp/internal/logger"
1517
)
1618

1719
// RouteAnalyzer parses Laravel route files
@@ -34,7 +36,7 @@ func (ra *RouteAnalyzer) Analyze(filePaths []string) ([]Route, error) {
3436
routes, err := ra.analyzeFile(path)
3537
if err != nil {
3638
// Log error but continue
37-
fmt.Fprintf(os.Stderr, "Error analyzing route file %s: %v\n", path, err)
39+
logger.Instance.Warn("[LARAVEL] Error analyzing route file %s: %v", path, err)
3840
continue
3941
}
4042
allRoutes = append(allRoutes, routes...)

pkg/parser/php/php_analyzer.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"path/filepath"
88
"strings"
99

10+
"github.com/doITmagic/rag-code-mcp/internal/logger"
1011
pkgParser "github.com/doITmagic/rag-code-mcp/pkg/parser"
1112
)
1213

@@ -21,6 +22,7 @@ var enrichers []FrameworkEnricher
2122
// RegisterEnricher adds a framework-specific enricher to the PHP parser.
2223
func RegisterEnricher(e FrameworkEnricher) {
2324
enrichers = append(enrichers, e)
25+
logger.Instance.Info("[PHP-ENRICHER] Registered enricher #%d: %T", len(enrichers), e)
2426
}
2527

2628
func init() {
@@ -52,26 +54,36 @@ func (a *Analyzer) CanHandle(filePath string) bool {
5254
// Analyze extracts symbols from a file or directory.
5355
func (a *Analyzer) Analyze(ctx context.Context, path string) (*pkgParser.Result, error) {
5456
paths := []string{path}
57+
logger.Instance.Debug("[PHP] Analyze: %s (enrichers=%d)", filepath.Base(path), len(enrichers))
58+
5559
chunks, err := a.codeAnalyzer.AnalyzePaths(paths)
5660
if err != nil {
61+
logger.Instance.Error("[PHP] Analyze ERROR: %s: %v", filepath.Base(path), err)
5762
return nil, err
5863
}
64+
logger.Instance.Debug("[PHP] Analyze: %s → %d base chunks, %d packages", filepath.Base(path), len(chunks), len(a.codeAnalyzer.GetPackages()))
5965

6066
// Fetch packages analyzed by the core PHP parser
6167
packages := a.codeAnalyzer.GetPackages()
6268

6369
// Run all registered framework enrichers
64-
for _, enricher := range enrichers {
65-
if enricher.IsApplicable(a.codeAnalyzer, paths) {
70+
for i, enricher := range enrichers {
71+
applicable := enricher.IsApplicable(a.codeAnalyzer, paths)
72+
logger.Instance.Debug("[PHP] Enricher #%d (%T) IsApplicable=%v for %s", i, enricher, applicable, filepath.Base(path))
73+
if applicable {
74+
before := len(chunks)
6675
chunks = enricher.Enrich(a.codeAnalyzer, packages, paths, chunks)
76+
logger.Instance.Info("[PHP] Enricher #%d (%T) enriched: %d → %d chunks for %s", i, enricher, before, len(chunks), filepath.Base(path))
6777
}
6878
}
6979

7080
// If no symbols found and the file is in a routes/ directory,
7181
// try extracting Route::* calls as symbols (Laravel convention).
7282
if len(chunks) == 0 && isRouteFile(path) {
83+
logger.Instance.Info("[PHP] Route fallback for %s", filepath.Base(path))
7384
routeChunks := a.codeAnalyzer.ExtractRouteChunks(path)
7485
chunks = append(chunks, routeChunks...)
86+
logger.Instance.Info("[PHP] Route fallback: +%d chunks", len(routeChunks))
7587
}
7688

7789
symbols := make([]pkgParser.Symbol, len(chunks))

0 commit comments

Comments
 (0)