@@ -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
2223type 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
2833func 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
12001207func (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
0 commit comments