- Formatted 103 files to comply with ruff format requirements - No code logic changes, only formatting/whitespace - Fixes CI formatting check failures
1909 lines
68 KiB
Python
1909 lines
68 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Design Pattern Recognition Module
|
|
|
|
Detects common design patterns in codebases across multiple languages.
|
|
|
|
Supported Patterns:
|
|
- Creational: Singleton, Factory, Builder, Prototype
|
|
- Structural: Adapter, Decorator, Facade, Proxy
|
|
- Behavioral: Observer, Strategy, Command, Template Method, Chain of Responsibility
|
|
|
|
Detection Levels:
|
|
- Surface: Naming conventions (e.g., "Factory", "Singleton")
|
|
- Deep: Structural analysis (class relationships, method signatures)
|
|
- Full: Behavioral analysis (method interactions, state management)
|
|
|
|
Credits:
|
|
- Design pattern definitions: Gang of Four (GoF) Design Patterns
|
|
- Detection heuristics: Inspired by academic research on pattern mining
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Confidence thresholds for pattern filtering (Issue #240)
|
|
CONFIDENCE_THRESHOLDS = {
|
|
"critical": 0.80, # High-confidence patterns for ARCHITECTURE.md
|
|
"high": 0.70, # Include in detailed analysis
|
|
"medium": 0.60, # Include with warning/context
|
|
"low": 0.50, # Minimum detection threshold
|
|
}
|
|
|
|
# Default minimum confidence for pattern detection
|
|
DEFAULT_MIN_CONFIDENCE = CONFIDENCE_THRESHOLDS["low"]
|
|
|
|
|
|
@dataclass
|
|
class PatternInstance:
|
|
"""Single detected pattern instance"""
|
|
|
|
pattern_type: str # e.g., 'Singleton', 'Factory'
|
|
category: str # 'Creational', 'Structural', 'Behavioral'
|
|
confidence: float # 0.0-1.0
|
|
location: str # File path
|
|
class_name: str | None = None
|
|
method_name: str | None = None
|
|
line_number: int | None = None
|
|
evidence: list[str] = field(default_factory=list) # Evidence for detection
|
|
related_classes: list[str] = field(default_factory=list) # Related pattern classes
|
|
ai_analysis: dict | None = None # AI-generated analysis (C3.6)
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Export to dictionary"""
|
|
result = {
|
|
"pattern_type": self.pattern_type,
|
|
"category": self.category,
|
|
"confidence": self.confidence,
|
|
"location": self.location,
|
|
"class_name": self.class_name,
|
|
"method_name": self.method_name,
|
|
"line_number": self.line_number,
|
|
"evidence": self.evidence,
|
|
"related_classes": self.related_classes,
|
|
}
|
|
if self.ai_analysis:
|
|
result["ai_analysis"] = self.ai_analysis
|
|
return result
|
|
|
|
|
|
@dataclass
|
|
class PatternReport:
|
|
"""Complete pattern detection report"""
|
|
|
|
file_path: str
|
|
language: str
|
|
patterns: list[PatternInstance]
|
|
total_classes: int
|
|
total_functions: int
|
|
analysis_depth: str # 'surface', 'deep', 'full'
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Export to dictionary"""
|
|
return {
|
|
"file_path": self.file_path,
|
|
"language": self.language,
|
|
"patterns": [p.to_dict() for p in self.patterns],
|
|
"total_classes": self.total_classes,
|
|
"total_functions": self.total_functions,
|
|
"analysis_depth": self.analysis_depth,
|
|
"pattern_summary": self.get_summary(),
|
|
}
|
|
|
|
def get_summary(self) -> dict[str, int]:
|
|
"""Get pattern count summary"""
|
|
summary = {}
|
|
for pattern in self.patterns:
|
|
summary[pattern.pattern_type] = summary.get(pattern.pattern_type, 0) + 1
|
|
return summary
|
|
|
|
|
|
class BasePatternDetector:
|
|
"""Base class for all pattern detectors"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
"""
|
|
Initialize detector.
|
|
|
|
Args:
|
|
depth: Detection depth ('surface', 'deep', 'full')
|
|
"""
|
|
self.depth = depth
|
|
self.pattern_type = "BasePattern"
|
|
self.category = "Unknown"
|
|
|
|
def detect_surface(self, _class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""
|
|
Surface-level detection using naming conventions.
|
|
|
|
Args:
|
|
class_sig: Class signature to analyze
|
|
all_classes: All classes in the file for context
|
|
|
|
Returns:
|
|
PatternInstance if pattern detected, None otherwise
|
|
"""
|
|
# Default: no surface detection
|
|
return None
|
|
|
|
def detect_deep(self, _class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""
|
|
Deep detection using structural analysis.
|
|
|
|
Args:
|
|
class_sig: Class signature to analyze
|
|
all_classes: All classes in the file for context
|
|
|
|
Returns:
|
|
PatternInstance if pattern detected, None otherwise
|
|
"""
|
|
# Default: no deep detection
|
|
return None
|
|
|
|
def detect_full(
|
|
self, _class_sig, _all_classes: list, _file_content: str
|
|
) -> PatternInstance | None:
|
|
"""
|
|
Full detection using behavioral analysis.
|
|
|
|
Args:
|
|
class_sig: Class signature to analyze
|
|
all_classes: All classes in the file for context
|
|
file_content: Full file content for advanced analysis
|
|
|
|
Returns:
|
|
PatternInstance if pattern detected, None otherwise
|
|
"""
|
|
# Default: no full detection
|
|
return None
|
|
|
|
def detect(
|
|
self, class_sig, all_classes: list, file_content: str | None = None
|
|
) -> PatternInstance | None:
|
|
"""
|
|
Detect pattern based on configured depth.
|
|
|
|
Args:
|
|
class_sig: Class signature to analyze
|
|
all_classes: All classes in the file for context
|
|
file_content: Full file content (needed for 'full' depth)
|
|
|
|
Returns:
|
|
PatternInstance if pattern detected, None otherwise
|
|
"""
|
|
if self.depth == "surface":
|
|
return self.detect_surface(class_sig, all_classes)
|
|
elif self.depth == "deep":
|
|
# Try deep first, fallback to surface
|
|
result = self.detect_deep(class_sig, all_classes)
|
|
if result:
|
|
return result
|
|
return self.detect_surface(class_sig, all_classes)
|
|
elif self.depth == "full":
|
|
# Try full, fallback to deep, then surface
|
|
if file_content:
|
|
result = self.detect_full(class_sig, all_classes, file_content)
|
|
if result:
|
|
return result
|
|
result = self.detect_deep(class_sig, all_classes)
|
|
if result:
|
|
return result
|
|
return self.detect_surface(class_sig, all_classes)
|
|
else:
|
|
raise ValueError(f"Invalid depth: {self.depth}")
|
|
|
|
|
|
class PatternRecognizer:
|
|
"""
|
|
Main pattern recognition orchestrator.
|
|
|
|
Coordinates multiple pattern detectors to analyze code.
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep", enhance_with_ai: bool = True):
|
|
"""
|
|
Initialize pattern recognizer.
|
|
|
|
Args:
|
|
depth: Detection depth ('surface', 'deep', 'full')
|
|
enhance_with_ai: Enable AI enhancement of detected patterns (default: True, C3.6)
|
|
"""
|
|
self.depth = depth
|
|
self.enhance_with_ai = enhance_with_ai
|
|
self.detectors: list[BasePatternDetector] = []
|
|
self._register_detectors()
|
|
|
|
# Initialize AI enhancer if enabled (C3.6)
|
|
self.ai_enhancer = None
|
|
if self.enhance_with_ai:
|
|
try:
|
|
from skill_seekers.cli.ai_enhancer import PatternEnhancer
|
|
|
|
self.ai_enhancer = PatternEnhancer()
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Failed to initialize AI enhancer: {e}")
|
|
self.enhance_with_ai = False
|
|
|
|
def _register_detectors(self):
|
|
"""Register all available pattern detectors"""
|
|
# Creational patterns (3)
|
|
self.detectors.append(SingletonDetector(self.depth))
|
|
self.detectors.append(FactoryDetector(self.depth))
|
|
self.detectors.append(BuilderDetector(self.depth))
|
|
|
|
# Structural patterns (2)
|
|
self.detectors.append(DecoratorDetector(self.depth))
|
|
self.detectors.append(AdapterDetector(self.depth))
|
|
|
|
# Behavioral patterns (5)
|
|
self.detectors.append(ObserverDetector(self.depth))
|
|
self.detectors.append(StrategyDetector(self.depth))
|
|
self.detectors.append(CommandDetector(self.depth))
|
|
self.detectors.append(TemplateMethodDetector(self.depth))
|
|
self.detectors.append(ChainOfResponsibilityDetector(self.depth))
|
|
|
|
def analyze_file(self, file_path: str, content: str, language: str) -> PatternReport:
|
|
"""
|
|
Analyze a single file for design patterns.
|
|
|
|
Args:
|
|
file_path: Path to source file
|
|
content: File content
|
|
language: Programming language
|
|
|
|
Returns:
|
|
PatternReport with detected patterns
|
|
"""
|
|
# Step 1: Analyze code structure using CodeAnalyzer
|
|
from skill_seekers.cli.code_analyzer import CodeAnalyzer
|
|
|
|
analyzer = CodeAnalyzer(depth="deep")
|
|
analysis = analyzer.analyze_file(file_path, content, language)
|
|
|
|
if not analysis:
|
|
return PatternReport(
|
|
file_path=file_path,
|
|
language=language,
|
|
patterns=[],
|
|
total_classes=0,
|
|
total_functions=0,
|
|
analysis_depth=self.depth,
|
|
)
|
|
|
|
classes = analysis.get("classes", [])
|
|
functions = analysis.get("functions", [])
|
|
|
|
# Convert to class signature objects
|
|
class_sigs = self._convert_to_signatures(classes)
|
|
|
|
# Step 2: Run pattern detection
|
|
detected_patterns = []
|
|
|
|
for class_sig in class_sigs:
|
|
for detector in self.detectors:
|
|
pattern = detector.detect(
|
|
class_sig=class_sig,
|
|
all_classes=class_sigs,
|
|
file_content=content if self.depth == "full" else None,
|
|
)
|
|
|
|
if pattern:
|
|
# Add file path to pattern
|
|
pattern.location = file_path
|
|
|
|
# Apply language-specific adaptations
|
|
pattern = LanguageAdapter.adapt_for_language(pattern, language)
|
|
|
|
detected_patterns.append(pattern)
|
|
|
|
# Step 3: Enhance patterns with AI analysis (C3.6)
|
|
if self.enhance_with_ai and self.ai_enhancer and detected_patterns:
|
|
# Convert patterns to dict format for AI processing
|
|
pattern_dicts = [p.to_dict() for p in detected_patterns]
|
|
enhanced_dicts = self.ai_enhancer.enhance_patterns(pattern_dicts)
|
|
|
|
# Update patterns with AI analysis
|
|
for i, pattern in enumerate(detected_patterns):
|
|
if i < len(enhanced_dicts) and "ai_analysis" in enhanced_dicts[i]:
|
|
pattern.ai_analysis = enhanced_dicts[i]["ai_analysis"]
|
|
# Apply confidence boost if provided
|
|
if "confidence" in enhanced_dicts[i]:
|
|
pattern.confidence = enhanced_dicts[i]["confidence"]
|
|
|
|
return PatternReport(
|
|
file_path=file_path,
|
|
language=language,
|
|
patterns=detected_patterns,
|
|
total_classes=len(classes),
|
|
total_functions=len(functions),
|
|
analysis_depth=self.depth,
|
|
)
|
|
|
|
def _convert_to_signatures(self, classes: list[dict]):
|
|
"""
|
|
Convert dict-based class analysis to signature objects.
|
|
|
|
Note: Returns simple namespace objects that mimic ClassSignature structure
|
|
but work with dict-based input from CodeAnalyzer.
|
|
"""
|
|
from types import SimpleNamespace
|
|
|
|
signatures = []
|
|
|
|
for cls in classes:
|
|
# Convert methods
|
|
methods = []
|
|
for method in cls.get("methods", []):
|
|
# Convert parameters
|
|
params = []
|
|
for param in method.get("parameters", []):
|
|
param_obj = SimpleNamespace(
|
|
name=param.get("name", ""),
|
|
type_hint=param.get("type_hint"),
|
|
default=param.get("default"),
|
|
)
|
|
params.append(param_obj)
|
|
|
|
method_obj = SimpleNamespace(
|
|
name=method.get("name", ""),
|
|
parameters=params,
|
|
return_type=method.get("return_type"),
|
|
docstring=method.get("docstring"),
|
|
line_number=method.get("line_number"),
|
|
is_async=method.get("is_async", False),
|
|
is_method=True,
|
|
decorators=method.get("decorators", []),
|
|
)
|
|
methods.append(method_obj)
|
|
|
|
class_obj = SimpleNamespace(
|
|
name=cls.get("name", ""),
|
|
base_classes=cls.get("base_classes", []),
|
|
methods=methods,
|
|
docstring=cls.get("docstring"),
|
|
line_number=cls.get("line_number"),
|
|
)
|
|
signatures.append(class_obj)
|
|
|
|
return signatures
|
|
|
|
|
|
class SingletonDetector(BasePatternDetector):
|
|
"""
|
|
Detect Singleton pattern.
|
|
|
|
Singleton ensures a class has only one instance and provides global access.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Class name contains 'Singleton'
|
|
- Deep: Private constructor + static instance method
|
|
- Full: Instance caching + thread safety checks
|
|
|
|
Examples:
|
|
- Python: __new__ override with instance caching
|
|
- JavaScript: Module pattern or class with getInstance()
|
|
- Java: Private constructor + synchronized getInstance()
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "Singleton"
|
|
self.category = "Creational"
|
|
|
|
def detect_surface(self, class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""Check if class name suggests Singleton"""
|
|
if "singleton" in class_sig.name.lower():
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.6,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=['Class name contains "Singleton"'],
|
|
)
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Check structural characteristics of Singleton"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
|
|
# Check for instance method (getInstance, instance, get_instance, etc.)
|
|
instance_methods = [
|
|
"getInstance",
|
|
"instance",
|
|
"get_instance",
|
|
"Instance",
|
|
"GetInstance",
|
|
"INSTANCE",
|
|
]
|
|
|
|
has_instance_method = False
|
|
for method in class_sig.methods:
|
|
if method.name in instance_methods:
|
|
evidence.append(f"Has instance method: {method.name}")
|
|
confidence += 0.4
|
|
has_instance_method = True
|
|
break
|
|
|
|
# Check for private/protected constructor-like methods
|
|
has_init_control = False
|
|
for method in class_sig.methods:
|
|
# Python: __init__ or __new__
|
|
# Java/C#: private constructor (detected by naming)
|
|
# Check if it has logic (not just pass)
|
|
if method.name in ["__new__", "__init__", "constructor"] and (
|
|
method.docstring or len(method.parameters) > 1
|
|
):
|
|
evidence.append(f"Controlled initialization: {method.name}")
|
|
confidence += 0.3
|
|
has_init_control = True
|
|
break
|
|
|
|
# Check for class-level instance storage
|
|
# This would require checking class attributes (future enhancement)
|
|
|
|
if has_instance_method or has_init_control and confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.9),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
)
|
|
|
|
# Fallback to surface detection
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
def detect_full(
|
|
self, class_sig, all_classes: list, file_content: str
|
|
) -> PatternInstance | None:
|
|
"""
|
|
Full behavioral analysis for Singleton.
|
|
|
|
Checks:
|
|
- Instance caching in method body
|
|
- Thread safety (locks, synchronized)
|
|
- Lazy vs eager initialization
|
|
"""
|
|
# Start with deep detection
|
|
result = self.detect_deep(class_sig, all_classes)
|
|
if not result:
|
|
return None
|
|
|
|
evidence = result.evidence.copy()
|
|
confidence = result.confidence
|
|
|
|
# Check for instance caching patterns in code
|
|
caching_patterns = [
|
|
"_instance",
|
|
"__instance",
|
|
"instance",
|
|
"if not",
|
|
"if self._instance is None",
|
|
"synchronized",
|
|
"Lock()",
|
|
"threading",
|
|
]
|
|
|
|
for pattern in caching_patterns:
|
|
if pattern in file_content and pattern not in " ".join(evidence):
|
|
evidence.append(f"Instance caching detected: {pattern}")
|
|
confidence += 0.1
|
|
|
|
# Cap confidence at 0.95 (never 100% certain without runtime analysis)
|
|
result.confidence = min(confidence, 0.95)
|
|
result.evidence = evidence
|
|
|
|
return result
|
|
|
|
|
|
class FactoryDetector(BasePatternDetector):
|
|
"""
|
|
Detect Factory pattern (Factory Method and Abstract Factory).
|
|
|
|
Factory defines an interface for creating objects, letting subclasses decide
|
|
which class to instantiate.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Class/method name contains 'Factory', 'create', 'make'
|
|
- Deep: Method returns different object types based on parameters
|
|
- Full: Polymorphic object creation with inheritance hierarchy
|
|
|
|
Examples:
|
|
- createProduct(type) -> Product
|
|
- ProductFactory with createProductA(), createProductB()
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "Factory"
|
|
self.category = "Creational"
|
|
|
|
def detect_surface(self, class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""Check naming conventions for Factory"""
|
|
# Check class name
|
|
if "factory" in class_sig.name.lower():
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.7,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=['Class name contains "Factory"'],
|
|
)
|
|
|
|
# Check for factory methods
|
|
factory_method_names = ["create", "make", "build", "new", "get"]
|
|
for method in class_sig.methods:
|
|
method_lower = method.name.lower()
|
|
# Check if method returns something (has return type or is not void)
|
|
if any(name in method_lower for name in factory_method_names) and (
|
|
method.return_type or "create" in method_lower
|
|
):
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.6,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
method_name=method.name,
|
|
line_number=method.line_number,
|
|
evidence=[f"Factory method detected: {method.name}"],
|
|
)
|
|
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Structural analysis for Factory"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
factory_methods = []
|
|
|
|
# Look for methods that create objects
|
|
creation_keywords = ["create", "make", "build", "new", "construct", "get"]
|
|
|
|
for method in class_sig.methods:
|
|
method_lower = method.name.lower()
|
|
|
|
# Check if method name suggests object creation
|
|
if any(keyword in method_lower for keyword in creation_keywords):
|
|
factory_methods.append(method.name)
|
|
confidence += 0.3
|
|
|
|
# Check if it takes parameters (suggests different object types)
|
|
if len(method.parameters) > 1: # >1 because 'self' counts
|
|
evidence.append(f"Parameterized factory method: {method.name}")
|
|
confidence += 0.2
|
|
else:
|
|
evidence.append(f"Factory method: {method.name}")
|
|
|
|
# Check if multiple factory methods exist (Abstract Factory pattern)
|
|
if len(factory_methods) >= 2:
|
|
evidence.append(f"Multiple factory methods: {', '.join(factory_methods[:3])}")
|
|
confidence += 0.2
|
|
|
|
# Check for inheritance (factory hierarchy)
|
|
if class_sig.base_classes:
|
|
evidence.append(f"Inherits from: {', '.join(class_sig.base_classes)}")
|
|
confidence += 0.1
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.9),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
related_classes=class_sig.base_classes,
|
|
)
|
|
|
|
# Fallback to surface
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
|
|
class ObserverDetector(BasePatternDetector):
|
|
"""
|
|
Detect Observer pattern (Pub/Sub).
|
|
|
|
Observer defines one-to-many dependency where multiple objects
|
|
observe and react to state changes.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Class/method names with 'Observer', 'Listener', 'Subscribe'
|
|
- Deep: attach/detach + notify methods
|
|
- Full: Collection of observers + iteration pattern
|
|
|
|
Examples:
|
|
- addObserver(), removeObserver(), notifyObservers()
|
|
- addEventListener(), removeEventListener(), emit()
|
|
- subscribe(), unsubscribe(), publish()
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "Observer"
|
|
self.category = "Behavioral"
|
|
|
|
def detect_surface(self, class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""Check naming for Observer pattern"""
|
|
observer_keywords = ["observer", "listener", "subscriber", "watcher"]
|
|
|
|
# Check class name
|
|
class_lower = class_sig.name.lower()
|
|
if any(keyword in class_lower for keyword in observer_keywords):
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.6,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=[f"Class name suggests Observer: {class_sig.name}"],
|
|
)
|
|
|
|
# Check method names
|
|
observer_methods = [
|
|
"subscribe",
|
|
"unsubscribe",
|
|
"publish",
|
|
"addobserver",
|
|
"removeobserver",
|
|
"notify",
|
|
"addeventlistener",
|
|
"removeeventlistener",
|
|
"emit",
|
|
"attach",
|
|
"detach",
|
|
"update",
|
|
]
|
|
|
|
for method in class_sig.methods:
|
|
method_lower = method.name.lower().replace("_", "")
|
|
if any(obs_method in method_lower for obs_method in observer_methods):
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.65,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
method_name=method.name,
|
|
line_number=method.line_number,
|
|
evidence=[f"Observer method detected: {method.name}"],
|
|
)
|
|
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Structural analysis for Observer"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
|
|
# Look for characteristic method triplet: attach/detach/notify
|
|
has_attach = False
|
|
has_detach = False
|
|
has_notify = False
|
|
|
|
attach_names = ["attach", "add", "subscribe", "register", "addeventlistener"]
|
|
detach_names = [
|
|
"detach",
|
|
"remove",
|
|
"unsubscribe",
|
|
"unregister",
|
|
"removeeventlistener",
|
|
]
|
|
notify_names = ["notify", "update", "emit", "publish", "fire", "trigger"]
|
|
|
|
for method in class_sig.methods:
|
|
method_lower = method.name.lower().replace("_", "")
|
|
|
|
if any(name in method_lower for name in attach_names):
|
|
has_attach = True
|
|
evidence.append(f"Attach method: {method.name}")
|
|
confidence += 0.3
|
|
|
|
if any(name in method_lower for name in detach_names):
|
|
has_detach = True
|
|
evidence.append(f"Detach method: {method.name}")
|
|
confidence += 0.3
|
|
|
|
if any(name in method_lower for name in notify_names):
|
|
has_notify = True
|
|
evidence.append(f"Notify method: {method.name}")
|
|
confidence += 0.3
|
|
|
|
# Strong signal if has all three
|
|
if has_attach and has_detach and has_notify:
|
|
confidence = min(confidence, 0.95)
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.95),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
)
|
|
|
|
# Fallback to surface
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
|
|
class StrategyDetector(BasePatternDetector):
|
|
"""
|
|
Detect Strategy pattern.
|
|
|
|
Strategy defines a family of algorithms, encapsulates each one,
|
|
and makes them interchangeable.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Class/method names with 'Strategy', 'Policy', 'Algorithm'
|
|
- Deep: Interface with single key method + multiple implementations
|
|
- Full: Composition with interchangeable strategy objects
|
|
|
|
Examples:
|
|
- SortStrategy with sort() method
|
|
- PaymentStrategy with pay() method
|
|
- CompressionStrategy with compress() method
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "Strategy"
|
|
self.category = "Behavioral"
|
|
|
|
def detect_surface(self, class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""Check naming for Strategy"""
|
|
strategy_keywords = ["strategy", "policy", "algorithm"]
|
|
|
|
class_lower = class_sig.name.lower()
|
|
if any(keyword in class_lower for keyword in strategy_keywords):
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.7,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=[f"Class name suggests Strategy: {class_sig.name}"],
|
|
)
|
|
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Structural analysis for Strategy"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
|
|
# Strategy pattern often involves:
|
|
# 1. Base class/interface with key method
|
|
# 2. Multiple subclasses implementing same interface
|
|
|
|
# Check if this class is a concrete strategy
|
|
if class_sig.base_classes:
|
|
base_class = class_sig.base_classes[0] if class_sig.base_classes else None
|
|
|
|
# Look for siblings (other strategies with same base)
|
|
siblings = [
|
|
cls.name
|
|
for cls in all_classes
|
|
if cls.base_classes
|
|
and base_class in cls.base_classes
|
|
and cls.name != class_sig.name
|
|
]
|
|
|
|
if siblings:
|
|
evidence.append(f"Part of strategy family with: {', '.join(siblings[:3])}")
|
|
confidence += 0.5
|
|
|
|
if base_class and ("strategy" in base_class.lower() or "policy" in base_class.lower()):
|
|
evidence.append(f"Inherits from strategy base: {base_class}")
|
|
confidence += 0.3
|
|
|
|
# Check if this is a strategy base class
|
|
# (has subclasses in same file)
|
|
subclasses = [cls.name for cls in all_classes if class_sig.name in cls.base_classes]
|
|
|
|
if len(subclasses) >= 2:
|
|
evidence.append(f"Strategy base with implementations: {', '.join(subclasses[:3])}")
|
|
confidence += 0.6
|
|
|
|
# Check for single dominant method (strategy interface)
|
|
if len(class_sig.methods) == 1 or len(class_sig.methods) == 2:
|
|
# Single method or method + __init__
|
|
main_method = [m for m in class_sig.methods if m.name not in ["__init__", "__new__"]]
|
|
if main_method:
|
|
evidence.append(f"Strategy interface method: {main_method[0].name}")
|
|
confidence += 0.2
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.9),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
related_classes=class_sig.base_classes + subclasses,
|
|
)
|
|
|
|
# Fallback to surface
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
|
|
class DecoratorDetector(BasePatternDetector):
|
|
"""
|
|
Detect Decorator pattern.
|
|
|
|
Decorator attaches additional responsibilities to an object dynamically,
|
|
providing flexible alternative to subclassing.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Class name contains 'Decorator', 'Wrapper'
|
|
- Deep: Wraps same interface, delegates to wrapped object
|
|
- Full: Composition + delegation + interface matching
|
|
|
|
Examples:
|
|
- LoggingDecorator wraps Service
|
|
- CachingDecorator wraps DataFetcher
|
|
- Python @decorator syntax
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "Decorator"
|
|
self.category = "Structural"
|
|
|
|
def detect_surface(self, class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""Check naming for Decorator"""
|
|
decorator_keywords = ["decorator", "wrapper", "proxy"]
|
|
|
|
class_lower = class_sig.name.lower()
|
|
if any(keyword in class_lower for keyword in decorator_keywords):
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.65,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=[f"Class name suggests Decorator: {class_sig.name}"],
|
|
)
|
|
|
|
# Check for Python decorator syntax
|
|
for method in class_sig.methods:
|
|
if method.decorators:
|
|
# Has decorators - might be using decorator pattern
|
|
# But this is too common, so low confidence
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.3,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
method_name=method.name,
|
|
line_number=method.line_number,
|
|
evidence=[f"Method uses decorators: {method.decorators}"],
|
|
)
|
|
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Structural analysis for Decorator"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
|
|
# Decorator pattern characteristics:
|
|
# 1. Has same base class as wrapped object
|
|
# 2. Takes wrapped object in constructor
|
|
# 3. Delegates calls to wrapped object
|
|
|
|
# Check if shares base class with other classes
|
|
if class_sig.base_classes:
|
|
base_class = class_sig.base_classes[0]
|
|
|
|
# Find other classes with same base
|
|
siblings = [
|
|
cls.name
|
|
for cls in all_classes
|
|
if cls.base_classes
|
|
and base_class in cls.base_classes
|
|
and cls.name != class_sig.name
|
|
]
|
|
|
|
if siblings:
|
|
evidence.append(f"Shares interface with: {', '.join(siblings[:2])}")
|
|
confidence += 0.3
|
|
|
|
# Check __init__ for composition (takes object parameter)
|
|
init_method = next((m for m in class_sig.methods if m.name == "__init__"), None)
|
|
# Check if takes object parameter (not just self)
|
|
if init_method and len(init_method.parameters) > 1: # More than just 'self'
|
|
param_names = [p.name for p in init_method.parameters if p.name != "self"]
|
|
if any(
|
|
name in ["wrapped", "component", "inner", "obj", "target"] for name in param_names
|
|
):
|
|
evidence.append(f"Takes wrapped object in constructor: {param_names}")
|
|
confidence += 0.4
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.85),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
related_classes=class_sig.base_classes,
|
|
)
|
|
|
|
# Fallback to surface
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
|
|
class BuilderDetector(BasePatternDetector):
|
|
"""
|
|
Detect Builder pattern.
|
|
|
|
Builder separates construction of complex object from its representation,
|
|
allowing same construction process to create different representations.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Class name contains 'Builder'
|
|
- Deep: Fluent interface (methods return self), build()/create() terminal method
|
|
- Full: Multiple configuration methods + final build step
|
|
|
|
Examples:
|
|
- QueryBuilder with where(), orderBy(), build()
|
|
- RequestBuilder with setHeader(), setBody(), execute()
|
|
- StringBuilder pattern
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "Builder"
|
|
self.category = "Creational"
|
|
|
|
def detect_surface(self, class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""Check naming for Builder"""
|
|
if "builder" in class_sig.name.lower():
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.7,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=[f'Class name contains "Builder": {class_sig.name}'],
|
|
)
|
|
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Structural analysis for Builder"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
|
|
# Builder characteristics:
|
|
# 1. Multiple setter/configuration methods
|
|
# 2. Terminal build()/create()/execute() method
|
|
# 3. Fluent interface (methods return self/this)
|
|
|
|
# Check for build/create terminal method
|
|
terminal_methods = ["build", "create", "execute", "construct", "make"]
|
|
has_terminal = any(
|
|
m.name.lower() in terminal_methods or m.name.lower().startswith("build")
|
|
for m in class_sig.methods
|
|
)
|
|
|
|
if has_terminal:
|
|
evidence.append("Has terminal build/create method")
|
|
confidence += 0.4
|
|
|
|
# Check for setter methods (with_, set_, add_)
|
|
setter_prefixes = ["with", "set", "add", "configure"]
|
|
setter_count = sum(
|
|
1
|
|
for m in class_sig.methods
|
|
if any(m.name.lower().startswith(prefix) for prefix in setter_prefixes)
|
|
)
|
|
|
|
if setter_count >= 3:
|
|
evidence.append(f"Has {setter_count} configuration methods")
|
|
confidence += 0.4
|
|
elif setter_count >= 1:
|
|
confidence += 0.2
|
|
|
|
# Check method count (builders typically have many methods)
|
|
if len(class_sig.methods) >= 5:
|
|
confidence += 0.1
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.9),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
)
|
|
|
|
# Fallback to surface
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
def detect_full(
|
|
self, class_sig, all_classes: list, file_content: str
|
|
) -> PatternInstance | None:
|
|
"""Full behavioral analysis for Builder"""
|
|
# Start with deep detection
|
|
pattern = self.detect_deep(class_sig, all_classes)
|
|
if not pattern:
|
|
return None
|
|
|
|
evidence = list(pattern.evidence)
|
|
confidence = pattern.confidence
|
|
|
|
# Look for fluent interface pattern (return self/this)
|
|
class_content = file_content.lower()
|
|
fluent_indicators = ["return self", "return this"]
|
|
|
|
if any(indicator in class_content for indicator in fluent_indicators):
|
|
evidence.append("Uses fluent interface (return self)")
|
|
confidence += 0.1
|
|
|
|
# Check for complex object construction (multiple fields)
|
|
if "self." in class_content and class_content.count("self.") >= 5:
|
|
evidence.append("Builds complex object with multiple fields")
|
|
confidence += 0.05
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.95),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
)
|
|
|
|
# Fallback to deep
|
|
return self.detect_deep(class_sig, all_classes)
|
|
|
|
|
|
class AdapterDetector(BasePatternDetector):
|
|
"""
|
|
Detect Adapter pattern.
|
|
|
|
Adapter converts interface of a class into another interface clients expect,
|
|
allowing incompatible interfaces to work together.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Class name contains 'Adapter', 'Wrapper'
|
|
- Deep: Wraps external/incompatible class, translates method calls
|
|
- Full: Composition + delegation with interface translation
|
|
|
|
Examples:
|
|
- DatabaseAdapter wraps external DB library
|
|
- ApiAdapter translates REST to internal interface
|
|
- FileSystemAdapter wraps OS file operations
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "Adapter"
|
|
self.category = "Structural"
|
|
|
|
def detect_surface(self, class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""Check naming for Adapter"""
|
|
adapter_keywords = ["adapter", "wrapper", "bridge"]
|
|
|
|
class_lower = class_sig.name.lower()
|
|
if any(keyword in class_lower for keyword in adapter_keywords):
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.7,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=[f"Class name suggests Adapter: {class_sig.name}"],
|
|
)
|
|
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Structural analysis for Adapter"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
|
|
# Adapter characteristics:
|
|
# 1. Takes adaptee in constructor
|
|
# 2. Implements target interface
|
|
# 3. Delegates to adaptee with translation
|
|
|
|
# Check __init__ for composition (takes adaptee)
|
|
init_method = next((m for m in class_sig.methods if m.name == "__init__"), None)
|
|
if init_method and len(init_method.parameters) > 1:
|
|
param_names = [p.name for p in init_method.parameters if p.name != "self"]
|
|
adaptee_names = ["adaptee", "wrapped", "client", "service", "api", "source"]
|
|
if any(name in param_names for name in adaptee_names):
|
|
evidence.append(f"Takes adaptee in constructor: {param_names}")
|
|
confidence += 0.4
|
|
|
|
# Check if implements interface (has base class)
|
|
if class_sig.base_classes:
|
|
evidence.append(f"Implements interface: {class_sig.base_classes[0]}")
|
|
confidence += 0.3
|
|
|
|
# Check for delegation methods (methods that likely call adaptee)
|
|
if len(class_sig.methods) >= 3: # Multiple interface methods
|
|
evidence.append(f"Has {len(class_sig.methods)} interface methods")
|
|
confidence += 0.2
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.85),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
)
|
|
|
|
# Fallback to surface
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
|
|
class CommandDetector(BasePatternDetector):
|
|
"""
|
|
Detect Command pattern.
|
|
|
|
Command encapsulates a request as an object, allowing parameterization
|
|
of clients with different requests, queuing, logging, and undo operations.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Class name contains 'Command', 'Action', 'Task'
|
|
- Deep: Has execute()/run() method, encapsulates action
|
|
- Full: Receiver composition + undo support
|
|
|
|
Examples:
|
|
- SaveCommand with execute() method
|
|
- UndoableCommand with undo() and redo()
|
|
- TaskCommand in task queue
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "Command"
|
|
self.category = "Behavioral"
|
|
|
|
def detect_surface(self, class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""Check naming for Command"""
|
|
command_keywords = ["command", "action", "task", "operation"]
|
|
|
|
class_lower = class_sig.name.lower()
|
|
if any(keyword in class_lower for keyword in command_keywords):
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.65,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=[f"Class name suggests Command: {class_sig.name}"],
|
|
)
|
|
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Structural analysis for Command"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
|
|
# Command characteristics:
|
|
# 1. Has execute()/run()/call() method
|
|
# 2. May have undo()/redo() methods
|
|
# 3. Encapsulates receiver and parameters
|
|
|
|
# Check for execute/run method
|
|
execute_methods = ["execute", "run", "call", "do", "perform", "__call__"]
|
|
has_execute = any(m.name.lower() in execute_methods for m in class_sig.methods)
|
|
|
|
if has_execute:
|
|
method_name = next(
|
|
m.name for m in class_sig.methods if m.name.lower() in execute_methods
|
|
)
|
|
evidence.append(f"Has execute method: {method_name}()")
|
|
confidence += 0.5
|
|
|
|
# Check for undo/redo support
|
|
undo_methods = ["undo", "rollback", "revert", "redo"]
|
|
has_undo = any(m.name.lower() in undo_methods for m in class_sig.methods)
|
|
|
|
if has_undo:
|
|
evidence.append("Supports undo/redo operations")
|
|
confidence += 0.3
|
|
|
|
# Check for receiver (takes object in __init__)
|
|
init_method = next((m for m in class_sig.methods if m.name == "__init__"), None)
|
|
if init_method and len(init_method.parameters) > 1:
|
|
evidence.append("Encapsulates receiver/parameters")
|
|
confidence += 0.2
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.9),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
)
|
|
|
|
# Fallback to surface
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
|
|
class TemplateMethodDetector(BasePatternDetector):
|
|
"""
|
|
Detect Template Method pattern.
|
|
|
|
Template Method defines skeleton of algorithm in base class,
|
|
letting subclasses override specific steps without changing structure.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Abstract/Base class with template-like names
|
|
- Deep: Abstract base with hook methods, concrete subclasses override
|
|
- Full: Template method calls abstract/hook methods
|
|
|
|
Examples:
|
|
- AbstractProcessor with process() calling abstract steps
|
|
- BaseParser with parse() template method
|
|
- Framework base classes with lifecycle hooks
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "TemplateMethod"
|
|
self.category = "Behavioral"
|
|
|
|
def detect_surface(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Check naming for Template Method"""
|
|
template_keywords = ["abstract", "base", "template"]
|
|
|
|
class_lower = class_sig.name.lower()
|
|
if any(keyword in class_lower for keyword in template_keywords):
|
|
# Check if has subclasses
|
|
subclasses = [cls.name for cls in all_classes if class_sig.name in cls.base_classes]
|
|
|
|
if subclasses:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.6,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=[f"Abstract base with subclasses: {', '.join(subclasses[:2])}"],
|
|
related_classes=subclasses,
|
|
)
|
|
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Structural analysis for Template Method"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
|
|
# Template Method characteristics:
|
|
# 1. Has subclasses (is base class)
|
|
# 2. Has methods that look like hooks (prepare, validate, cleanup, etc.)
|
|
# 3. Has template method that orchestrates
|
|
|
|
# Check for subclasses
|
|
subclasses = [cls.name for cls in all_classes if class_sig.name in cls.base_classes]
|
|
|
|
if len(subclasses) >= 1:
|
|
evidence.append(f"Base class with {len(subclasses)} implementations")
|
|
confidence += 0.4
|
|
|
|
# Check for hook-like method names
|
|
hook_keywords = [
|
|
"prepare",
|
|
"initialize",
|
|
"validate",
|
|
"process",
|
|
"finalize",
|
|
"setup",
|
|
"teardown",
|
|
"before",
|
|
"after",
|
|
"pre",
|
|
"post",
|
|
"hook",
|
|
]
|
|
|
|
hook_methods = [
|
|
m.name
|
|
for m in class_sig.methods
|
|
if any(keyword in m.name.lower() for keyword in hook_keywords)
|
|
]
|
|
|
|
if len(hook_methods) >= 2:
|
|
evidence.append(f"Has hook methods: {', '.join(hook_methods[:3])}")
|
|
confidence += 0.3
|
|
|
|
# Check for abstract methods (no implementation or pass/raise)
|
|
abstract_methods = [
|
|
m.name
|
|
for m in class_sig.methods
|
|
if m.name.startswith("_") or "abstract" in m.name.lower()
|
|
]
|
|
|
|
if abstract_methods:
|
|
evidence.append(f"Has abstract methods: {', '.join(abstract_methods[:2])}")
|
|
confidence += 0.2
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.85),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
related_classes=subclasses,
|
|
)
|
|
|
|
# Fallback to surface
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
|
|
class ChainOfResponsibilityDetector(BasePatternDetector):
|
|
"""
|
|
Detect Chain of Responsibility pattern.
|
|
|
|
Chain of Responsibility passes request along chain of handlers until
|
|
one handles it, avoiding coupling sender to receiver.
|
|
|
|
Detection Heuristics:
|
|
- Surface: Class name contains 'Handler', 'Chain', 'Middleware'
|
|
- Deep: Has next/successor reference, handle() method
|
|
- Full: Chain traversal logic, request passing
|
|
|
|
Examples:
|
|
- LogHandler with next handler
|
|
- AuthMiddleware chain
|
|
- EventHandler chain
|
|
"""
|
|
|
|
def __init__(self, depth: str = "deep"):
|
|
super().__init__(depth)
|
|
self.pattern_type = "ChainOfResponsibility"
|
|
self.category = "Behavioral"
|
|
|
|
def detect_surface(self, class_sig, _all_classes: list) -> PatternInstance | None:
|
|
"""Check naming for Chain of Responsibility"""
|
|
chain_keywords = ["handler", "chain", "middleware", "filter", "processor"]
|
|
|
|
class_lower = class_sig.name.lower()
|
|
if any(keyword in class_lower for keyword in chain_keywords):
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=0.6,
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=[f"Class name suggests handler chain: {class_sig.name}"],
|
|
)
|
|
|
|
return None
|
|
|
|
def detect_deep(self, class_sig, all_classes: list) -> PatternInstance | None:
|
|
"""Structural analysis for Chain of Responsibility"""
|
|
evidence = []
|
|
confidence = 0.0
|
|
|
|
# Chain of Responsibility characteristics:
|
|
# 1. Has handle()/process() method
|
|
# 2. Has next/successor reference
|
|
# 3. May have set_next() method
|
|
|
|
# Check for handle/process method
|
|
handle_methods = ["handle", "process", "execute", "filter", "middleware"]
|
|
has_handle = any(
|
|
m.name.lower() in handle_methods or m.name.lower().startswith("handle")
|
|
for m in class_sig.methods
|
|
)
|
|
|
|
if has_handle:
|
|
evidence.append("Has handle/process method")
|
|
confidence += 0.4
|
|
|
|
# Check for next/successor methods or parameters
|
|
init_method = next((m for m in class_sig.methods if m.name == "__init__"), None)
|
|
has_next_ref = False
|
|
|
|
if init_method:
|
|
param_names = [p.name for p in init_method.parameters if p.name != "self"]
|
|
next_names = ["next", "successor", "next_handler", "next_middleware"]
|
|
|
|
if any(name in param_names for name in next_names):
|
|
evidence.append("Takes next handler in chain")
|
|
confidence += 0.3
|
|
has_next_ref = True
|
|
|
|
# Check for set_next() method
|
|
has_set_next = any(
|
|
"next" in m.name.lower() and ("set" in m.name.lower() or "add" in m.name.lower())
|
|
for m in class_sig.methods
|
|
)
|
|
|
|
if has_set_next:
|
|
evidence.append("Has set_next() method")
|
|
confidence += 0.3
|
|
has_next_ref = True
|
|
|
|
# Check if part of handler family (shares base class)
|
|
if class_sig.base_classes:
|
|
base_class = class_sig.base_classes[0]
|
|
siblings = [
|
|
cls.name
|
|
for cls in all_classes
|
|
if cls.base_classes
|
|
and base_class in cls.base_classes
|
|
and cls.name != class_sig.name
|
|
]
|
|
|
|
if siblings and has_next_ref:
|
|
evidence.append(f"Part of handler chain with: {', '.join(siblings[:2])}")
|
|
confidence += 0.2
|
|
|
|
if confidence >= 0.5:
|
|
return PatternInstance(
|
|
pattern_type=self.pattern_type,
|
|
category=self.category,
|
|
confidence=min(confidence, 0.9),
|
|
location="",
|
|
class_name=class_sig.name,
|
|
line_number=class_sig.line_number,
|
|
evidence=evidence,
|
|
)
|
|
|
|
# Fallback to surface
|
|
return self.detect_surface(class_sig, all_classes)
|
|
|
|
|
|
class LanguageAdapter:
|
|
"""
|
|
Language-specific pattern detection adaptations.
|
|
|
|
Adjusts pattern confidence based on language idioms and conventions.
|
|
Different languages have different ways of implementing patterns.
|
|
"""
|
|
|
|
@staticmethod
|
|
def adapt_for_language(pattern: PatternInstance, language: str) -> PatternInstance:
|
|
"""
|
|
Adjust confidence based on language-specific idioms.
|
|
|
|
Args:
|
|
pattern: Detected pattern instance
|
|
language: Programming language
|
|
|
|
Returns:
|
|
Adjusted pattern instance with language-specific confidence
|
|
"""
|
|
if not pattern:
|
|
return pattern
|
|
|
|
evidence_str = " ".join(pattern.evidence).lower()
|
|
|
|
# Python-specific adaptations
|
|
if language == "Python":
|
|
# Decorator pattern: Python has native @ syntax
|
|
if pattern.pattern_type == "Decorator":
|
|
if "@" in " ".join(pattern.evidence):
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
pattern.evidence.append("Python @decorator syntax detected")
|
|
|
|
# Singleton: __new__ method is Python idiom
|
|
elif pattern.pattern_type == "Singleton":
|
|
if "__new__" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
|
|
# Strategy: Duck typing common in Python
|
|
elif (
|
|
pattern.pattern_type == "Strategy"
|
|
and "duck typing" in evidence_str
|
|
or "protocol" in evidence_str
|
|
):
|
|
pattern.confidence = min(pattern.confidence + 0.05, 1.0)
|
|
|
|
# JavaScript/TypeScript adaptations
|
|
elif language in ["JavaScript", "TypeScript"]:
|
|
# Singleton: Module pattern is common
|
|
if pattern.pattern_type == "Singleton":
|
|
if "module" in evidence_str or "export default" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
pattern.evidence.append("JavaScript module pattern")
|
|
|
|
# Factory: Factory functions are idiomatic
|
|
elif pattern.pattern_type == "Factory":
|
|
if "create" in evidence_str or "make" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.05, 1.0)
|
|
|
|
# Observer: Event emitters are built-in
|
|
elif (
|
|
pattern.pattern_type == "Observer"
|
|
and "eventemitter" in evidence_str
|
|
or "event" in evidence_str
|
|
):
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
pattern.evidence.append("EventEmitter pattern detected")
|
|
|
|
# Java/C# adaptations (interface-heavy languages)
|
|
elif language in ["Java", "C#"]:
|
|
# All patterns: Interfaces are explicit
|
|
if "interface" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.05, 1.0)
|
|
|
|
# Factory: Abstract Factory common
|
|
if pattern.pattern_type == "Factory":
|
|
if "abstract" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
pattern.evidence.append("Abstract Factory pattern")
|
|
|
|
# Template Method: Abstract classes common
|
|
elif pattern.pattern_type == "TemplateMethod" and "abstract" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
|
|
# Go adaptations
|
|
elif language == "Go":
|
|
# Singleton: sync.Once is idiomatic
|
|
if pattern.pattern_type == "Singleton":
|
|
if "sync.once" in evidence_str or "once.do" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.15, 1.0)
|
|
pattern.evidence.append("Go sync.Once idiom")
|
|
|
|
# Strategy: Interfaces are implicit
|
|
elif pattern.pattern_type == "Strategy" and "interface{}" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.05, 1.0)
|
|
|
|
# Rust adaptations
|
|
elif language == "Rust":
|
|
# Singleton: Lazy static is common
|
|
if pattern.pattern_type == "Singleton":
|
|
if "lazy_static" in evidence_str or "oncecell" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.15, 1.0)
|
|
pattern.evidence.append("Rust lazy_static/OnceCell")
|
|
|
|
# Builder: Derive builder is idiomatic
|
|
elif pattern.pattern_type == "Builder":
|
|
if "derive" in evidence_str and "builder" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
|
|
# Adapter: Trait adapters are common
|
|
elif pattern.pattern_type == "Adapter" and "trait" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
|
|
# C++ adaptations
|
|
elif language == "C++":
|
|
# Singleton: Meyer's Singleton is idiomatic
|
|
if pattern.pattern_type == "Singleton":
|
|
if "static" in evidence_str and "local" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
pattern.evidence.append("Meyer's Singleton (static local)")
|
|
|
|
# Factory: Template-based factories
|
|
elif pattern.pattern_type == "Factory" and "template" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.05, 1.0)
|
|
|
|
# Ruby adaptations
|
|
elif language == "Ruby":
|
|
# Singleton: Ruby has Singleton module
|
|
if pattern.pattern_type == "Singleton":
|
|
if "include singleton" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.2, 1.0)
|
|
pattern.evidence.append("Ruby Singleton module")
|
|
|
|
# Builder: Method chaining is idiomatic
|
|
elif pattern.pattern_type == "Builder" and "method chaining" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.05, 1.0)
|
|
|
|
# PHP adaptations
|
|
elif language == "PHP":
|
|
# Singleton: Private constructor is common
|
|
if pattern.pattern_type == "Singleton":
|
|
if "private" in evidence_str and "__construct" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.1, 1.0)
|
|
|
|
# Factory: Static factory methods
|
|
elif pattern.pattern_type == "Factory" and "static" in evidence_str:
|
|
pattern.confidence = min(pattern.confidence + 0.05, 1.0)
|
|
|
|
return pattern
|
|
|
|
|
|
# ============================================================================
|
|
# PATTERN FILTERING UTILITIES (Issue #240 - C4.2)
|
|
# ============================================================================
|
|
|
|
|
|
def filter_patterns_by_confidence(patterns: list[dict], min_confidence: float) -> list[dict]:
|
|
"""
|
|
Filter patterns by minimum confidence threshold.
|
|
|
|
Args:
|
|
patterns: List of pattern dictionaries (from PatternReport.to_dict())
|
|
min_confidence: Minimum confidence threshold (0.0-1.0)
|
|
|
|
Returns:
|
|
Filtered list of patterns meeting the threshold
|
|
"""
|
|
filtered = []
|
|
for pattern in patterns:
|
|
if pattern.get("confidence", 0.0) >= min_confidence:
|
|
filtered.append(pattern)
|
|
return filtered
|
|
|
|
|
|
def create_multi_level_report(pattern_results: list[dict]) -> dict:
|
|
"""
|
|
Create multi-level pattern report with different confidence thresholds.
|
|
|
|
Args:
|
|
pattern_results: List of PatternReport dictionaries
|
|
|
|
Returns:
|
|
Dictionary with patterns grouped by confidence level:
|
|
- all_patterns: All detected patterns
|
|
- high_confidence: Patterns >= 0.70 (for detailed analysis)
|
|
- critical: Patterns >= 0.80 (for ARCHITECTURE.md)
|
|
- statistics: Pattern count by level
|
|
"""
|
|
# Flatten all patterns from all files
|
|
all_patterns = []
|
|
for report in pattern_results:
|
|
file_path = report.get("file_path", "unknown")
|
|
for pattern in report.get("patterns", []):
|
|
# Add file path to pattern for context
|
|
pattern_with_file = {**pattern, "file_path": file_path}
|
|
all_patterns.append(pattern_with_file)
|
|
|
|
# Sort by confidence (highest first)
|
|
all_patterns_sorted = sorted(all_patterns, key=lambda p: p.get("confidence", 0.0), reverse=True)
|
|
|
|
# Filter by confidence levels
|
|
critical = filter_patterns_by_confidence(all_patterns_sorted, CONFIDENCE_THRESHOLDS["critical"])
|
|
high_confidence = filter_patterns_by_confidence(
|
|
all_patterns_sorted, CONFIDENCE_THRESHOLDS["high"]
|
|
)
|
|
medium = filter_patterns_by_confidence(all_patterns_sorted, CONFIDENCE_THRESHOLDS["medium"])
|
|
|
|
return {
|
|
"all_patterns": all_patterns_sorted,
|
|
"critical": critical,
|
|
"high_confidence": high_confidence,
|
|
"medium": medium,
|
|
"statistics": {
|
|
"total": len(all_patterns_sorted),
|
|
"critical_count": len(critical),
|
|
"high_confidence_count": len(high_confidence),
|
|
"medium_count": len(medium),
|
|
"low_count": len(all_patterns_sorted) - len(medium),
|
|
},
|
|
"thresholds": CONFIDENCE_THRESHOLDS,
|
|
}
|
|
|
|
|
|
def main():
|
|
"""
|
|
CLI entry point for pattern detection.
|
|
|
|
Usage:
|
|
skill-seekers-patterns --file src/database.py
|
|
skill-seekers-patterns --directory src/ --output patterns/
|
|
skill-seekers-patterns --file app.py --depth full --json
|
|
"""
|
|
import sys
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Detect design patterns in source code",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
# Analyze single file
|
|
skill-seekers-patterns --file src/database.py
|
|
|
|
# Analyze directory
|
|
skill-seekers-patterns --directory src/ --output patterns/
|
|
|
|
# Full analysis with JSON output
|
|
skill-seekers-patterns --file app.py --depth full --json
|
|
|
|
# Multiple files
|
|
skill-seekers-patterns --file src/db.py --file src/api.py
|
|
|
|
Supported Languages:
|
|
Python, JavaScript, TypeScript, C++, C, C#, Go, Rust, Java, Ruby, PHP
|
|
""",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--file",
|
|
action="append",
|
|
help="Source file to analyze (can be specified multiple times)",
|
|
)
|
|
parser.add_argument("--directory", help="Directory to analyze (analyzes all source files)")
|
|
parser.add_argument(
|
|
"--output", help="Output directory for results (default: current directory)"
|
|
)
|
|
parser.add_argument(
|
|
"--depth",
|
|
choices=["surface", "deep", "full"],
|
|
default="deep",
|
|
help="Detection depth: surface (fast), deep (default), full (thorough)",
|
|
)
|
|
parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Output JSON format instead of human-readable",
|
|
)
|
|
parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Validate inputs
|
|
if not args.file and not args.directory:
|
|
parser.error("Must specify either --file or --directory")
|
|
|
|
# Create recognizer
|
|
recognizer = PatternRecognizer(depth=args.depth)
|
|
|
|
# Collect files to analyze
|
|
files_to_analyze = []
|
|
|
|
if args.file:
|
|
for file_path in args.file:
|
|
path = Path(file_path)
|
|
if not path.exists():
|
|
print(f"Error: File not found: {file_path}", file=sys.stderr)
|
|
return 1
|
|
files_to_analyze.append(path)
|
|
|
|
if args.directory:
|
|
from skill_seekers.cli.codebase_scraper import detect_language, walk_directory
|
|
|
|
directory = Path(args.directory)
|
|
if not directory.exists():
|
|
print(f"Error: Directory not found: {args.directory}", file=sys.stderr)
|
|
return 1
|
|
|
|
# Walk directory for source files
|
|
files_to_analyze.extend(walk_directory(directory))
|
|
|
|
if not files_to_analyze:
|
|
print("No source files found to analyze", file=sys.stderr)
|
|
return 1
|
|
|
|
# Analyze files
|
|
all_reports = []
|
|
total_patterns = 0
|
|
|
|
for file_path in files_to_analyze:
|
|
try:
|
|
from skill_seekers.cli.codebase_scraper import detect_language
|
|
|
|
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
language = detect_language(file_path)
|
|
|
|
if language == "Unknown":
|
|
if args.verbose:
|
|
print(f"Skipping {file_path}: Unknown language")
|
|
continue
|
|
|
|
report = recognizer.analyze_file(str(file_path), content, language)
|
|
|
|
if report.patterns:
|
|
all_reports.append(report)
|
|
total_patterns += len(report.patterns)
|
|
|
|
if not args.json and args.verbose:
|
|
print(f"\n{file_path}:")
|
|
for pattern in report.patterns:
|
|
print(
|
|
f" [{pattern.pattern_type}] {pattern.class_name} (confidence: {pattern.confidence:.2f})"
|
|
)
|
|
|
|
except Exception as e:
|
|
if args.verbose:
|
|
print(f"Error analyzing {file_path}: {e}", file=sys.stderr)
|
|
continue
|
|
|
|
# Output results
|
|
if args.json:
|
|
# JSON output
|
|
output_data = {
|
|
"total_files_analyzed": len(files_to_analyze),
|
|
"files_with_patterns": len(all_reports),
|
|
"total_patterns_detected": total_patterns,
|
|
"reports": [report.to_dict() for report in all_reports],
|
|
}
|
|
|
|
if args.output:
|
|
output_path = Path(args.output) / "detected_patterns.json"
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
json.dump(output_data, f, indent=2)
|
|
print(f"Results saved to: {output_path}")
|
|
else:
|
|
print(json.dumps(output_data, indent=2))
|
|
|
|
else:
|
|
# Human-readable output
|
|
print(f"\n{'=' * 60}")
|
|
print("PATTERN DETECTION RESULTS")
|
|
print(f"{'=' * 60}")
|
|
print(f"Files analyzed: {len(files_to_analyze)}")
|
|
print(f"Files with patterns: {len(all_reports)}")
|
|
print(f"Total patterns detected: {total_patterns}")
|
|
print(f"{'=' * 60}\n")
|
|
|
|
# Pattern summary by type
|
|
pattern_counts = {}
|
|
for report in all_reports:
|
|
for pattern in report.patterns:
|
|
pattern_counts[pattern.pattern_type] = (
|
|
pattern_counts.get(pattern.pattern_type, 0) + 1
|
|
)
|
|
|
|
if pattern_counts:
|
|
print("Pattern Summary:")
|
|
for pattern_type, count in sorted(
|
|
pattern_counts.items(), key=lambda x: x[1], reverse=True
|
|
):
|
|
print(f" {pattern_type}: {count}")
|
|
print()
|
|
|
|
# Detailed results
|
|
if all_reports:
|
|
print("Detected Patterns:\n")
|
|
for report in all_reports:
|
|
print(f"{report.file_path}:")
|
|
for pattern in report.patterns:
|
|
print(f" • {pattern.pattern_type} - {pattern.class_name}")
|
|
print(f" Confidence: {pattern.confidence:.2f}")
|
|
print(f" Category: {pattern.category}")
|
|
if pattern.evidence:
|
|
print(f" Evidence: {pattern.evidence[0]}")
|
|
print()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|