feat: Extend framework detection to 5 languages (JavaScript, Java, Ruby, PHP, C#)

## Summary
Framework detection now works for **6 languages** (up from 1):
-  Python (original)
-  JavaScript/TypeScript (new)
-  Java (new)
-  Ruby (new)
-  PHP (new)
-  C# (new)

## Changes

### 1. JavaScript/TypeScript Import Extraction (code_analyzer.py:361-386)
Detects:
- ES6 imports: `import React from 'react'`
- Side-effect imports: `import 'style.css'`
- CommonJS: `const foo = require('bar')`

Extracts package names: `react`, `vue`, `angular`, `express`, `axios`, etc.

### 2. Java Import Extraction (code_analyzer.py:1093-1110)
Detects:
- Package imports: `import org.springframework.boot.*;`
- Static imports: `import static com.example.Util.*;`

Extracts base packages: `org.springframework`, `com.google`, etc.

### 3. Ruby Import Extraction (code_analyzer.py:1245-1258)
Detects:
- Require: `require 'rails'`
- Require relative: `require_relative 'config'`

Extracts gem names: `rails`, `sinatra`, etc.

### 4. PHP Import Extraction (code_analyzer.py:1368-1381)
Detects:
- Namespace use: `use Laravel\Framework\App;`
- Aliased use: `use Foo\Bar as Baz;`

Extracts vendor names: `laravel`, `symfony`, etc.

### 5. C# Import Extraction (code_analyzer.py:677-696)
Detects:
- Using directives: `using System.Collections.Generic;`
- Static using: `using static System.Math;`

Extracts namespaces: `System.Collections`, `Microsoft.AspNetCore`, etc.

### 6. Enhanced Framework Markers (architectural_pattern_detector.py:104-111)
Added import-based markers for better detection:
- **Spring**: Added `org.springframework`
- **ASP.NET**: Added `Microsoft.AspNetCore`, `System.Web`
- **Rails**: Added `action` (for ActionController, ActionMailer)
- **Angular**: Added `@angular`, `angular`
- **Laravel**: Added `illuminate`, `laravel`

### 7. Multi-Language Support (architectural_pattern_detector.py:202-210)
Framework detector now:
- Collects imports from **all languages** (not just Python)
- Logs: "Collected N imports from M files"
- Detects frameworks across polyglot projects

## Test Results

**Multi-language test project:**
```
react_app/App.jsx       → React detected 
spring_app/Application.java → Spring detected 
rails_app/controller.rb → Rails detected 
```

**Output:**
```json
{
  "frameworks_detected": ["Spring", "Rails", "React"]
}
```

**All tests passing:**
-  95 tests (38 + 54 + 3)
-  No breaking changes
-  Backward compatible

## Impact

### What This Enables

1. **Polyglot project support** - Detect multiple frameworks in monorepos
2. **Better accuracy** - Import-based detection is more reliable than path-based
3. **Technology Stack insights** - ARCHITECTURE.md now shows all frameworks used
4. **Multi-platform coverage** - Works for web, mobile, backend, enterprise

### Supported Frameworks by Language

**JavaScript/TypeScript:**
- React, Vue.js, Angular (frontend)
- Express, Nest.js (backend)

**Java:**
- Spring Framework (Spring Boot, Spring MVC, etc.)

**Ruby:**
- Ruby on Rails

**PHP:**
- Laravel

**C#:**
- ASP.NET (Core, MVC, Web API)

**Python:**
- Django, Flask

### Example Use Cases

**Full-stack project:**
```
frontend/ (React)     → React detected
backend/ (Spring)     → Spring detected
Result: ["React", "Spring"]
```

**Microservices:**
```
api-gateway/ (Express)  → Express detected
auth-service/ (Spring)  → Spring detected
user-service/ (Rails)   → Rails detected
Result: ["Express", "Spring", "Rails"]
```

## Future Extensions

Ready to add:
- Go: `import "github.com/gin-gonic/gin"`
- Rust: `use actix_web::*;`
- Swift: `import SwiftUI`
- Kotlin: `import kotlinx.coroutines.*`

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
yusyus
2026-02-05 22:08:37 +03:00
parent a565b87a90
commit fda3712367
2 changed files with 114 additions and 14 deletions

View File

@@ -101,14 +101,14 @@ class ArchitecturalPatternDetector:
# Web Frameworks
"Django": ["django", "manage.py", "settings.py", "urls.py"],
"Flask": ["flask", "app.py", "wsgi.py"],
"Spring": ["springframework", "@Controller", "@Service", "@Repository"],
"ASP.NET": ["Controllers", "Models", "Views", ".cshtml", "Startup.cs"],
"Rails": ["app/models", "app/views", "app/controllers", "config/routes.rb"],
"Angular": ["app.module.ts", "@Component", "@Injectable", "angular.json"],
"React": ["package.json", "react", "components"],
"Spring": ["springframework", "org.springframework", "@Controller", "@Service", "@Repository"],
"ASP.NET": ["Microsoft.AspNetCore", "System.Web", "Controllers", "Models", "Views", ".cshtml", "Startup.cs"],
"Rails": ["rails", "action", "app/models", "app/views", "app/controllers", "config/routes.rb"],
"Angular": ["@angular", "angular", "app.module.ts", "@Component", "@Injectable", "angular.json"],
"React": ["react", "package.json", "components"],
"Vue.js": ["vue", ".vue", "components"],
"Express": ["express", "app.js", "routes"],
"Laravel": ["artisan", "app/Http/Controllers", "app/Models"],
"Laravel": ["laravel", "illuminate", "artisan", "app/Http/Controllers", "app/Models"],
}
def __init__(self, enhance_with_ai: bool = True):
@@ -200,15 +200,15 @@ class ArchitecturalPatternDetector:
all_paths = [str(f.get("file", "")) for f in files]
all_content = " ".join(all_paths)
# Extract all imports from Python files (fixes #239)
# Extract all imports from ALL languages (fixes #239 - extended to multi-language)
all_imports = []
for file_data in files:
if file_data.get("language") == "Python" and file_data.get("imports"):
if file_data.get("imports"):
all_imports.extend(file_data["imports"])
# Create searchable import string
import_content = " ".join(all_imports)
logger.debug(f"Collected {len(all_imports)} imports for framework detection")
logger.debug(f"Collected {len(all_imports)} imports from {len([f for f in files if f.get('imports')])} files for framework detection")
# Also check actual directory structure for game engine markers
# (project.godot, .unity, .uproject are config files, not in analyzed files)

View File

@@ -361,7 +361,31 @@ class CodeAnalyzer:
# Extract comments
comments = self._extract_js_comments(content)
return {"classes": classes, "functions": functions, "comments": comments}
# Extract imports for framework detection
imports = []
# Match: import foo from 'bar'
# Match: import { foo } from 'bar'
# Match: import * as foo from 'bar'
# Match: const foo = require('bar')
import_patterns = [
r"import\s+.*?\s+from\s+['\"]([^'\"]+)['\"]", # ES6 imports
r"import\s+['\"]([^'\"]+)['\"]", # Side-effect imports
r"require\(['\"]([^'\"]+)['\"]\)", # CommonJS require
]
for pattern in import_patterns:
for match in re.finditer(pattern, content):
module = match.group(1)
# Extract package name (before first /)
package = module.split('/')[0]
if package and not package.startswith('.'): # Skip relative imports
imports.append(package)
return {
"classes": classes,
"functions": functions,
"comments": comments,
"imports": list(set(imports)), # Deduplicate
}
def _extract_js_methods(self, class_body: str) -> list[dict]:
"""Extract method signatures from class body."""
@@ -662,7 +686,29 @@ class CodeAnalyzer:
# Extract comments
comments = self._extract_csharp_comments(content)
return {"classes": classes, "functions": functions, "comments": comments}
# Extract imports for framework detection
imports = []
# Match: using System.Collections.Generic;
# Match: using static System.Math;
using_pattern = r"using\s+(?:static\s+)?([^;=]+);"
for match in re.finditer(using_pattern, content):
namespace = match.group(1).strip()
# Skip using aliases (using Foo = Bar.Baz)
if '=' not in namespace:
# Extract base namespace (first 1-2 segments)
parts = namespace.split('.')
if len(parts) >= 2:
base_ns = '.'.join(parts[:2])
imports.append(base_ns)
elif len(parts) == 1:
imports.append(parts[0])
return {
"classes": classes,
"functions": functions,
"comments": comments,
"imports": list(set(imports)), # Deduplicate
}
def _extract_csharp_methods(self, class_body: str) -> list[dict]:
"""Extract C# method signatures from class body."""
@@ -1076,7 +1122,26 @@ class CodeAnalyzer:
# Extract comments
comments = self._extract_java_comments(content)
return {"classes": classes, "functions": functions, "comments": comments}
# Extract imports for framework detection
imports = []
# Match: import com.example.Foo;
# Match: import static com.example.Foo.bar;
import_pattern = r"import\s+(?:static\s+)?([^;]+);"
for match in re.finditer(import_pattern, content):
import_path = match.group(1).strip()
# Extract package name (first 2-3 segments for framework detection)
parts = import_path.split('.')
if len(parts) >= 2:
# Get base package (e.g., "org.springframework" from "org.springframework.boot.SpringApplication")
package = '.'.join(parts[:2])
imports.append(package)
return {
"classes": classes,
"functions": functions,
"comments": comments,
"imports": list(set(imports)), # Deduplicate
}
def _extract_java_methods(self, class_body: str) -> list[dict]:
"""Extract Java method signatures from class body."""
@@ -1229,7 +1294,24 @@ class CodeAnalyzer:
# Extract comments
comments = self._extract_ruby_comments(content)
return {"classes": classes, "functions": functions, "comments": comments}
# Extract imports for framework detection
imports = []
# Match: require 'foo'
# Match: require "foo"
# Match: require_relative 'foo'
require_pattern = r"require(?:_relative)?\s+['\"]([^'\"]+)['\"]"
for match in re.finditer(require_pattern, content):
module = match.group(1)
# Extract gem name (before first /)
gem = module.split('/')[0]
imports.append(gem)
return {
"classes": classes,
"functions": functions,
"comments": comments,
"imports": list(set(imports)), # Deduplicate
}
def _parse_ruby_parameters(self, params_str: str) -> list[dict]:
"""Parse Ruby parameter string."""
@@ -1353,7 +1435,25 @@ class CodeAnalyzer:
# Extract comments
comments = self._extract_php_comments(content)
return {"classes": classes, "functions": functions, "comments": comments}
# Extract imports for framework detection
imports = []
# Match: use Foo\Bar\Baz;
# Match: use Foo\Bar\Baz as Alias;
use_pattern = r"use\s+([^;]+?)(?:\s+as\s+\w+)?;"
for match in re.finditer(use_pattern, content):
namespace = match.group(1).strip()
# Extract vendor name (first segment)
parts = namespace.split('\\')
if parts:
vendor = parts[0]
imports.append(vendor.lower())
return {
"classes": classes,
"functions": functions,
"comments": comments,
"imports": list(set(imports)), # Deduplicate
}
def _extract_php_methods(self, class_body: str) -> list[dict]:
"""Extract PHP method signatures from class body."""