salim0986/graph-bug-ai
0
1"""2Code Analyzer - Advanced code analysis for PR reviews3Combines AST parsing, graph queries, and vector search for comprehensive analysis4"""5 6from typing import List, Dict, Any, Optional, Tuple7from pydantic import BaseModel8from tree_sitter import Node9from .graph_builder import GraphBuilder10from .vector_builder import VectorBuilder11from .parser import UniversalParser12from .logger import setup_logger13import re14 15logger = setup_logger(__name__)16 17 18# ============================================================================19# REQUEST/RESPONSE MODELS20# ============================================================================21 22class FileChange(BaseModel):23 """Represents a changed file in a PR"""24 filename: str25 status: str # 'added', 'removed', 'modified', 'renamed'26 additions: int27 deletions: int28 patch: Optional[str] = None29 language: Optional[str] = None30 31 32class PRAnalysisRequest(BaseModel):33 """Request for analyzing an entire PR"""34 repo_id: str35 pr_number: int36 files: List[FileChange]37 base_ref: str38 head_ref: str39 40 41class FileAnalysisRequest(BaseModel):42 """Request for analyzing a single file"""43 repo_id: str44 filename: str45 content: str46 language: str47 48 49class DiffAnalysisRequest(BaseModel):50 """Request for analyzing a diff/patch"""51 repo_id: str52 filename: str53 patch: str54 language: str55 56 57class CodeIssue(BaseModel):58 """A single code issue found during analysis"""59 severity: str # 'critical', 'high', 'medium', 'low', 'info'60 category: str # 'security', 'performance', 'bug', 'code_quality', etc.61 title: str62 description: str63 line_number: Optional[int] = None64 line_range: Optional[Tuple[int, int]] = None65 suggestion: Optional[str] = None66 code_snippet: Optional[str] = None67 68 69class SimilarCode(BaseModel):70 """Similar code found via vector search"""71 file: str # Changed from filename for consistency72 name: str # Changed from function_name73 similarity_score: float74 code_snippet: Optional[str] = None75 line: int # Changed from line_number76 reason: Optional[str] = None # Add reason field77 78 79class RelatedCode(BaseModel):80 """Related code found via graph traversal"""81 file: str # Changed from filename for consistency82 type: str # Changed from node_type83 name: str84 reason: str # Changed from relationship for better clarity85 line: Optional[int] = None # Changed from line_number, made optional86 87 88class FileAnalysisResult(BaseModel):89 """Result of analyzing a single file"""90 filename: str91 language: str92 issues: List[CodeIssue]93 similar_code: List[SimilarCode]94 related_code: List[RelatedCode]95 metrics: Dict[str, Any]96 97 98class PRAnalysisResult(BaseModel):99 """Complete PR analysis result"""100 pr_number: int101 files_analyzed: int102 total_issues: int103 issues_by_severity: Dict[str, int]104 issues_by_category: Dict[str, int]105 file_results: List[FileAnalysisResult]106 overall_metrics: Dict[str, Any]107 108 109# ============================================================================110# CODE ANALYZER CLASS111# ============================================================================112 113class CodeAnalyzer:114 """115 Main code analysis engine116 Combines multiple analysis techniques for comprehensive code review117 """118 119 def __init__(120 self,121 graph_db: GraphBuilder,122 vector_db: VectorBuilder,123 parser: UniversalParser124 ):125 self.graph_db = graph_db126 self.vector_db = vector_db127 self.parser = parser128 129 # ------------------------------------------------------------------------130 # PR ANALYSIS131 # ------------------------------------------------------------------------132 133 async def analyze_pr(self, request: PRAnalysisRequest) -> PRAnalysisResult:134 """135 Analyze an entire PR136 Returns comprehensive analysis with issues, patterns, and metrics137 """138 logger.info(f"Analyzing PR #{request.pr_number} for repo {request.repo_id}")139 140 file_results: List[FileAnalysisResult] = []141 total_issues = 0142 issues_by_severity: Dict[str, int] = {}143 issues_by_category: Dict[str, int] = {}144 145 # Analyze each changed file146 for file_change in request.files:147 # Skip removed files148 if file_change.status == "removed":149 continue150 151 # Skip non-reviewable files152 if not self._is_reviewable_file(file_change.filename):153 continue154 155 try:156 # Analyze the file157 if file_change.patch:158 result = await self.analyze_diff(DiffAnalysisRequest(159 repo_id=request.repo_id,160 filename=file_change.filename,161 patch=file_change.patch,162 language=file_change.language or "unknown"163 ))164 else:165 # If no patch, do basic file analysis166 result = FileAnalysisResult(167 filename=file_change.filename,168 language=file_change.language or "unknown",169 issues=[],170 similar_code=[],171 related_code=[],172 metrics={}173 )174 175 file_results.append(result)176 total_issues += len(result.issues)177 178 # Aggregate by severity and category179 for issue in result.issues:180 issues_by_severity[issue.severity] = issues_by_severity.get(issue.severity, 0) + 1181 issues_by_category[issue.category] = issues_by_category.get(issue.category, 0) + 1182 183 except Exception as e:184 logger.error(f"Error analyzing {file_change.filename}: {e}")185 continue186 187 # Calculate overall metrics188 overall_metrics = {189 "total_files": len(request.files),190 "files_analyzed": len(file_results),191 "total_additions": sum(f.additions for f in request.files),192 "total_deletions": sum(f.deletions for f in request.files),193 }194 195 # Add impact analysis across the PR196 try:197 impact_analysis = self._analyze_pr_impact(request.repo_id, request.files)198 overall_metrics.update(impact_analysis)199 except Exception as e:200 logger.error(f"Error calculating PR impact: {e}")201 202 return PRAnalysisResult(203 pr_number=request.pr_number,204 files_analyzed=len(file_results),205 total_issues=total_issues,206 issues_by_severity=issues_by_severity,207 issues_by_category=issues_by_category,208 file_results=file_results,209 overall_metrics=overall_metrics210 )211 212 # ------------------------------------------------------------------------213 # FILE ANALYSIS214 # ------------------------------------------------------------------------215 216 async def analyze_file(self, request: FileAnalysisRequest) -> FileAnalysisResult:217 """218 Analyze a complete file219 Returns issues, similar code, and related code220 """221 logger.info(f"Analyzing file {request.filename} for repo {request.repo_id}")222 223 issues: List[CodeIssue] = []224 similar_code: List[SimilarCode] = []225 related_code: List[RelatedCode] = []226 metrics: Dict[str, Any] = {}227 228 # 1. Detect code smells and anti-patterns229 issues.extend(self._detect_code_smells(request.content, request.language))230 231 # 2. Find similar code via vector search232 similar_code.extend(await self._find_similar_code(233 request.repo_id,234 request.filename,235 request.content236 ))237 238 # 3. Find related code via graph queries239 related_code.extend(await self._find_related_code(240 request.repo_id,241 request.filename242 ))243 244 # 4. Calculate metrics245 metrics = self._calculate_file_metrics(request.content, request.language)246 247 return FileAnalysisResult(248 filename=request.filename,249 language=request.language,250 issues=issues,251 similar_code=similar_code,252 related_code=related_code,253 metrics=metrics254 )255 256 # ------------------------------------------------------------------------257 # DIFF ANALYSIS258 # ------------------------------------------------------------------------259 260 async def analyze_diff(self, request: DiffAnalysisRequest) -> FileAnalysisResult:261 """262 Analyze a diff/patch263 Focus on changed lines and their context264 """265 logger.info(f"Analyzing diff for {request.filename} in repo {request.repo_id}")266 267 issues: List[CodeIssue] = []268 similar_code: List[SimilarCode] = []269 related_code: List[RelatedCode] = []270 metrics: Dict[str, Any] = {}271 272 # Parse the patch to extract changed lines273 changed_lines = self._parse_patch(request.patch)274 275 # Extract code from added lines276 added_code = "\n".join([line['content'] for line in changed_lines if line['type'] == 'add'])277 278 if added_code:279 # 1. Detect issues in added code280 issues.extend(self._detect_code_smells(added_code, request.language))281 282 # 2. Find similar code283 similar_code.extend(await self._find_similar_code(284 request.repo_id,285 request.filename,286 added_code287 ))288 289 # 3. Find related code for the entire file290 related_code.extend(await self._find_related_code(291 request.repo_id,292 request.filename293 ))294 295 # 4. Calculate metrics for the diff296 metrics = {297 "lines_added": len([l for l in changed_lines if l['type'] == 'add']),298 "lines_removed": len([l for l in changed_lines if l['type'] == 'delete']),299 "hunks": len(self._extract_hunks(request.patch))300 }301 302 return FileAnalysisResult(303 filename=request.filename,304 language=request.language,305 issues=issues,306 similar_code=similar_code,307 related_code=related_code,308 metrics=metrics309 )310 311 # ------------------------------------------------------------------------312 # PRIVATE HELPER METHODS313 # ------------------------------------------------------------------------314 315 def _is_reviewable_file(self, filename: str) -> bool:316 """Check if file should be reviewed"""317 # Skip lock files318 if any(lock in filename for lock in ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Gemfile.lock']):319 return False320 321 # Skip build artifacts322 if any(pattern in filename for pattern in ['dist/', 'build/', '.next/', 'target/', 'bin/', 'obj/']):323 return False324 325 # Skip generated files326 if any(pattern in filename for pattern in ['.generated.', '.min.js', '.bundle.js']):327 return False328 329 return True330 331 def _detect_code_smells(self, code: str, language: str) -> List[CodeIssue]:332 """333 Detect code smells and anti-patterns using both regex and AST analysis334 Enhanced with cyclomatic complexity and language-specific patterns335 """336 issues: List[CodeIssue] = []337 lines = code.split('\n')338 339 # 1. Basic metrics340 if len(lines) > 50:341 issues.append(CodeIssue(342 severity="medium",343 category="code_quality",344 title="Long function detected",345 description=f"This code block is {len(lines)} lines long. Consider breaking it into smaller functions.",346 suggestion="Refactor into smaller, focused functions for better maintainability."347 ))348 349 # 2. Deep nesting detection350 for i, line in enumerate(lines, 1):351 indent_level = (len(line) - len(line.lstrip())) // 4352 if indent_level > 4:353 issues.append(CodeIssue(354 severity="low",355 category="code_quality",356 title="Deep nesting detected",357 description=f"Line {i} has {indent_level} levels of indentation.",358 line_number=i,359 suggestion="Consider extracting nested logic into separate functions."360 ))361 break362 363 # 3. Cyclomatic complexity (AST-based)364 try:365 complexity = self._calculate_cyclomatic_complexity(code, language)366 if complexity > 10:367 issues.append(CodeIssue(368 severity="high" if complexity > 20 else "medium",369 category="code_quality",370 title=f"High cyclomatic complexity: {complexity}",371 description=f"This code has a cyclomatic complexity of {complexity}. High complexity makes code harder to test and maintain.",372 suggestion="Refactor to reduce branching logic. Consider extracting conditional blocks into separate functions."373 ))374 except Exception as e:375 logger.debug(f"Could not calculate complexity: {e}")376 377 # 4. Enhanced security patterns378 security_patterns = [379 # Code injection380 (r'eval\s*\(', "Use of eval() is a security risk", "critical", "Never use eval(). Use JSON.parse() or safe alternatives."),381 (r'exec\s*\(', "Use of exec() is a security risk", "critical", "Avoid exec(). Use subprocess with proper input validation."),382 (r'__import__\s*\(', "Dynamic imports can be dangerous", "high", "Avoid dynamic imports with user input."),383 384 # Hardcoded secrets385 (r'password\s*=\s*["\'][^"\']["\']', "Hardcoded password detected", "critical", "Use environment variables or secret management systems."),386 (r'api[_-]?key\s*=\s*["\'][^"\']["\']', "Hardcoded API key detected", "critical", "Store API keys in environment variables or secure vaults."),387 (r'secret\s*=\s*["\'][^"\']["\']', "Hardcoded secret detected", "critical", "Use secure secret management."),388 (r'token\s*=\s*["\'][A-Za-z0-9]{20,}["\']', "Hardcoded token detected", "critical", "Store tokens securely."),389 390 # XSS vulnerabilities391 (r'\.innerHTML\s*=', "Direct innerHTML assignment can lead to XSS", "high", "Use textContent or DOMPurify for sanitization."),392 (r'dangerouslySetInnerHTML', "Dangerous HTML injection risk", "high", "Sanitize user input before rendering."),393 (r'document\.write\s*\(', "document.write is unsafe and deprecated", "high", "Use modern DOM manipulation methods."),394 395 # SQL injection396 (r'execute\s*\(\s*["\'].*%s.*["\']', "Potential SQL injection", "critical", "Use parameterized queries or ORM."),397 (r'query\s*\(\s*f["\']', "f-string in SQL query", "critical", "Use parameterized queries to prevent SQL injection."),398 (r'\+\s*request\.|\+\s*req\.', "String concatenation with request data", "high", "Validate and sanitize user input."),399 400 # Path traversal401 (r'open\s*\(.*\+.*\)', "Potential path traversal", "high", "Validate file paths and use os.path.join() safely."),402 (r'readFile\s*\(.*\+', "File operation with concatenation", "high", "Validate file paths to prevent traversal attacks."),403 404 # Insecure crypto405 (r'md5\s*\(', "MD5 is cryptographically broken", "high", "Use SHA-256 or bcrypt for hashing."),406 (r'sha1\s*\(', "SHA-1 is weak", "medium", "Use SHA-256 or stronger algorithms."),407 408 # Unsafe deserialization409 (r'pickle\.loads?\s*\(', "Pickle deserialization is unsafe", "high", "Use JSON or validate pickle sources."),410 (r'yaml\.load\s*\([^,)]*\)', "yaml.load without Loader is unsafe", "high", "Use yaml.safe_load() instead."),411 ]412 413 for pattern, description, severity, suggestion in security_patterns:414 for i, line in enumerate(lines, 1):415 if re.search(pattern, line, re.IGNORECASE):416 issues.append(CodeIssue(417 severity=severity,418 category="security",419 title="Security vulnerability detected",420 description=description,421 line_number=i,422 code_snippet=line.strip(),423 suggestion=suggestion424 ))425 426 # 5. Performance anti-patterns427 performance_patterns = [428 (r'for\s+.*\s+in\s+range\s*\(\s*len\s*\(', "Use enumerate() instead of range(len())", "low", "for i, item in enumerate(items):"),429 (r'\[.*for.*in.*if.*\].*\[.*for.*in.*\]', "Nested list comprehensions", "low", "Consider breaking into multiple steps for readability."),430 (r'\.append\s*\(.*\)\s*$', "Repeated append in loop", "info", "Consider list comprehension for better performance."),431 ]432 433 for pattern, description, severity, suggestion in performance_patterns:434 for i, line in enumerate(lines, 1):435 if re.search(pattern, line):436 issues.append(CodeIssue(437 severity=severity,438 category="performance",439 title="Performance concern",440 description=description,441 line_number=i,442 suggestion=suggestion443 ))444 445 # 6. Language-specific patterns446 issues.extend(self._detect_language_specific_issues(code, language, lines))447 448 # 7. Code quality patterns449 quality_patterns = [450 (r'except:\s*$', "Bare except clause", "medium", "Catch specific exceptions instead of using bare except."),451 (r'except\s+Exception:\s*pass', "Silent exception swallowing", "high", "Log exceptions or handle them properly."),452 (r'print\s*\(', "Debug print statement", "info", "Use proper logging instead of print statements."),453 (r'console\.log\s*\(', "Debug console.log", "info", "Remove debug logs before production."),454 (r'debugger;?', "Debugger statement", "medium", "Remove debugger statements before committing."),455 (r'var\s+\w+\s*=', "Using 'var' instead of 'let'/'const'", "low", "Use 'let' or 'const' for better scoping."),456 (r'==(?!=)', "Using '==' instead of '==='", "low", "Use strict equality '===' to avoid type coercion."),457 ]458 459 for pattern, description, severity, suggestion in quality_patterns:460 for i, line in enumerate(lines, 1):461 if re.search(pattern, line):462 issues.append(CodeIssue(463 severity=severity,464 category="code_quality",465 title=description,466 description=description,467 line_number=i,468 code_snippet=line.strip(),469 suggestion=suggestion470 ))471 472 # 8. TODO/FIXME comments473 for i, line in enumerate(lines, 1):474 if re.search(r'TODO|FIXME|HACK|XXX', line, re.IGNORECASE):475 issues.append(CodeIssue(476 severity="info",477 category="maintainability",478 title="TODO comment found",479 description="Code contains TODO/FIXME comment indicating incomplete work.",480 line_number=i,481 code_snippet=line.strip()482 ))483 484 return issues485 486 def _calculate_cyclomatic_complexity(self, code: str, language: str) -> int:487 """488 Calculate cyclomatic complexity using AST analysis489 Counts decision points: if, for, while, case, catch, &&, ||490 Base complexity = 1491 """492 complexity = 1493 494 try:495 # Try to parse with tree-sitter496 from tree_sitter_languages import get_language, get_parser497 498 # Map our language names to tree-sitter language names499 lang_map = {500 'javascript': 'javascript',501 'typescript': 'typescript',502 'python': 'python',503 'java': 'java',504 'go': 'go',505 'rust': 'rust',506 'cpp': 'cpp',507 'c': 'c',508 'ruby': 'ruby',509 'php': 'php'510 }511 512 tree_sitter_lang = lang_map.get(language.lower())513 if not tree_sitter_lang:514 # Fallback to regex-based counting515 return self._calculate_complexity_regex(code, language)516 517 parser = get_parser(tree_sitter_lang)518 tree = parser.parse(bytes(code, 'utf8'))519 520 # Count decision points in AST521 complexity += self._count_decision_points(tree.root_node)522 523 except Exception as e:524 logger.debug(f"AST parsing failed, using regex fallback: {e}")525 complexity = self._calculate_complexity_regex(code, language)526 527 return complexity528 529 def _count_decision_points(self, node) -> int:530 """Recursively count decision points in AST"""531 count = 0532 533 # Decision point node types across languages534 decision_nodes = {535 'if_statement', 'elif_clause', 'else_clause',536 'for_statement', 'while_statement', 'do_statement',537 'case_statement', 'switch_statement',538 'catch_clause', 'except_clause',539 'conditional_expression', 'ternary_expression',540 'boolean_operator', 'binary_expression'541 }542 543 if node.type in decision_nodes:544 count += 1545 546 # Check for boolean operators (&&, ||)547 if node.type == 'binary_expression':548 if node.text and (b'&&' in node.text or b'||' in node.text or b'and' in node.text or b'or' in node.text):549 count += 1550 551 # Recurse through children552 for child in node.children:553 count += self._count_decision_points(child)554 555 return count556 557 def _calculate_complexity_regex(self, code: str, language: str) -> int:558 """Fallback complexity calculation using regex"""559 complexity = 1560 561 # Common patterns across languages562 patterns = [563 r'\bif\b', r'\belif\b', r'\belse\s+if\b',564 r'\bfor\b', r'\bwhile\b', r'\bdo\b',565 r'\bcase\b', r'\bswitch\b',566 r'\bcatch\b', r'\bexcept\b',567 r'\?.*:', r'&&', r'\|\|',568 r'\band\b', r'\bor\b'569 ]570 571 for pattern in patterns:572 complexity += len(re.findall(pattern, code, re.IGNORECASE))573 574 return complexity575 576 def _detect_language_specific_issues(self, code: str, language: str, lines: List[str]) -> List[CodeIssue]:577 """578 Detect language-specific code smells and anti-patterns579 """580 issues = []581 582 if language.lower() in ['python', 'py']:583 issues.extend(self._detect_python_issues(lines))584 elif language.lower() in ['javascript', 'typescript', 'js', 'ts']:585 issues.extend(self._detect_javascript_issues(lines))586 elif language.lower() in ['java']:587 issues.extend(self._detect_java_issues(lines))588 elif language.lower() in ['go']:589 issues.extend(self._detect_go_issues(lines))590 591 return issues592 593 def _detect_python_issues(self, lines: List[str]) -> List[CodeIssue]:594 """Python-specific code smell detection"""595 issues = []596 597 patterns = [598 (r'from\s+\*\s+import', "Wildcard import", "low", "Import specific names instead of using wildcard imports."),599 (r'\btype\s*\(\s*\w+\s*\)\s*==', "Using type() for type checking", "low", "Use isinstance() instead of type() for type checking."),600 (r'len\s*\(\s*\w+\s*\)\s*==\s*0', "Using len() to check empty", "info", "Use 'if not my_list:' instead of 'if len(my_list) == 0'."),601 (r'\.has_key\s*\(', "Deprecated has_key()", "medium", "Use 'key in dict' instead of dict.has_key(key)."),602 (r'except.*,', "Old-style exception syntax", "medium", "Use 'except Exception as e:' instead of 'except Exception, e:'."),603 ]604 605 for i, line in enumerate(lines, 1):606 for pattern, description, severity, suggestion in patterns:607 if re.search(pattern, line):608 issues.append(CodeIssue(609 severity=severity,610 category="code_quality",611 title=f"Python: {description}",612 description=description,613 line_number=i,614 code_snippet=line.strip(),615 suggestion=suggestion616 ))617 618 return issues619 620 def _detect_javascript_issues(self, lines: List[str]) -> List[CodeIssue]:621 """JavaScript/TypeScript-specific code smell detection"""622 issues = []623 624 patterns = [625 (r'new\s+Array\s*\(\)', "Use array literal []", "info", "Use [] instead of new Array()."),626 (r'new\s+Object\s*\(\)', "Use object literal {}", "info", "Use {} instead of new Object()."),627 (r'\.innerHTML\s*\+=', "Inefficient innerHTML concatenation", "medium", "Use DocumentFragment or insertAdjacentHTML."),628 (r'for\s*\(\s*var\s+\w+\s+in\s+', "for-in loop on array", "medium", "Use for-of or forEach for arrays."),629 (r'setTimeout\s*\([^,]*,\s*0\s*\)', "setTimeout(..., 0) hack", "low", "Consider using Promise.resolve().then() or proper async handling."),630 (r'\$\(.*\)\.\w+\s*\([^)]*\)\.\w+\s*\([^)]*\)\.\w+', "jQuery chain too long", "low", "Long jQuery chains are hard to debug."),631 ]632 633 for i, line in enumerate(lines, 1):634 for pattern, description, severity, suggestion in patterns:635 if re.search(pattern, line):636 issues.append(CodeIssue(637 severity=severity,638 category="code_quality",639 title=f"JavaScript: {description}",640 description=description,641 line_number=i,642 code_snippet=line.strip(),643 suggestion=suggestion644 ))645 646 return issues647 648 def _detect_java_issues(self, lines: List[str]) -> List[CodeIssue]:649 """Java-specific code smell detection"""650 issues = []651 652 patterns = [653 (r'System\.out\.print', "Using System.out.print", "info", "Use proper logging framework instead of System.out."),654 (r'\.printStackTrace\s*\(\)', "Printing stack trace", "medium", "Use proper logging instead of printStackTrace()."),655 (r'new\s+String\s*\(', "Unnecessary String construction", "low", "String literals are automatically interned."),656 (r'\+\s*""\s*\+', "String concatenation with empty string", "low", "Use String.valueOf() for type conversion."),657 ]658 659 for i, line in enumerate(lines, 1):660 for pattern, description, severity, suggestion in patterns:661 if re.search(pattern, line):662 issues.append(CodeIssue(663 severity=severity,664 category="code_quality",665 title=f"Java: {description}",666 description=description,667 line_number=i,668 code_snippet=line.strip(),669 suggestion=suggestion670 ))671 672 return issues673 674 def _detect_go_issues(self, lines: List[str]) -> List[CodeIssue]:675 """Go-specific code smell detection"""676 issues = []677 678 patterns = [679 (r'panic\s*\(', "Using panic()", "high", "Return errors instead of using panic() for recoverable errors."),680 (r'_\s*=.*err', "Ignoring error", "critical", "Always handle errors properly in Go."),681 (r'defer.*Close\(\).*\n.*err\s*:=', "Defer after error check", "medium", "Check errors before deferring Close()."),682 ]683 684 for i, line in enumerate(lines, 1):685 for pattern, description, severity, suggestion in patterns:686 if re.search(pattern, line):687 issues.append(CodeIssue(688 severity=severity,689 category="code_quality",690 title=f"Go: {description}",691 description=description,692 line_number=i,693 code_snippet=line.strip(),694 suggestion=suggestion695 ))696 697 return issues698 699 async def _find_similar_code(700 self,701 repo_id: str,702 filename: str,703 code: str704 ) -> List[SimilarCode]:705 """706 Find similar code using vector search707 Detects potential duplicates, similar patterns, and related implementations708 """709 similar: List[SimilarCode] = []710 711 try:712 # Find similar code snippets (70% similarity threshold)713 similar_results = self.vector_db.search_similar_code(714 repo_id=repo_id,715 code_snippet=code,716 limit=5,717 min_score=0.7718 )719 720 logger.info(f"[SIMILAR_CODE] Vector search returned {len(similar_results)} results for {filename}")721 722 for result in similar_results:723 # Skip if it's the same file and line (exact match)724 if result.get("file") == filename and abs(result.get("line", 0) - 1) < 5:725 continue726 727 similar.append(SimilarCode(728 file=result.get("file", "unknown"),729 name=result.get("name", "unknown"),730 similarity_score=result.get("similarity", 0.0),731 code_snippet=result.get("code", "")[:200] if result.get("code") else None,732 line=result.get("line", 0),733 reason=self._get_similarity_reason(result.get("similarity", 0.0))734 ))735 736 # Also check for high-confidence duplicates (90%+ similarity)737 if code.strip(): # Only check non-empty code738 duplicates = self.vector_db.find_duplicate_code(739 repo_id=repo_id,740 code_snippet=code,741 limit=3,742 threshold=0.9743 )744 745 for dup in duplicates:746 # Skip if already added or same location747 if dup.get("file") == filename:748 continue749 750 if not any(s.file == dup.get("file") and s.line == dup.get("line") for s in similar):751 similar.append(SimilarCode(752 file=dup.get("file", "unknown"),753 name=dup.get("name", "unknown"),754 similarity_score=dup.get("similarity", 0.0),755 code_snippet=dup.get("code", "")[:200] if dup.get("code") else None,756 line=dup.get("line", 0),757 reason="Potential duplicate code - consider refactoring"758 ))759 760 if similar:761 logger.info(f"[SIMILAR_CODE] Returning {len(similar)} similar code items for {filename}")762 else:763 logger.warning(f"[SIMILAR_CODE] No similar code found for {filename}")764 765 except Exception as e:766 logger.error(f"Error finding similar code: {e}")767 768 return similar[:10] # Limit to top 10 results769 770 def _get_similarity_reason(self, score: float) -> str:771 """Generate human-readable reason for similarity"""772 if score >= 0.95:773 return "Nearly identical code - strong duplicate candidate"774 elif score >= 0.85:775 return "Very similar implementation - consider refactoring"776 elif score >= 0.75:777 return "Similar code pattern - review for consistency"778 else:779 return "Related implementation"780 781 def _calculate_cyclomatic_complexity(self, code: str, language: str) -> int:782 """783 Calculate cyclomatic complexity using AST analysis784 Counts decision points: if, for, while, case, catch, &&, ||785 Base complexity = 1786 """787 complexity = 1788 789 try:790 # Try to parse with tree-sitter791 from tree_sitter_languages import get_language, get_parser792 793 # Map our language names to tree-sitter language names794 lang_map = {795 'javascript': 'javascript',796 'typescript': 'typescript',797 'python': 'python',798 'java': 'java',799 'go': 'go',800 'rust': 'rust',801 'cpp': 'cpp',802 'c': 'c',803 'ruby': 'ruby',804 'php': 'php'805 }806 807 tree_sitter_lang = lang_map.get(language.lower())808 if not tree_sitter_lang:809 # Fallback to regex-based counting810 return self._calculate_complexity_regex(code, language)811 812 parser = get_parser(tree_sitter_lang)813 tree = parser.parse(bytes(code, 'utf8'))814 815 # Count decision points in AST816 complexity += self._count_decision_points(tree.root_node)817 818 except Exception as e:819 logger.debug(f"AST parsing failed, using regex fallback: {e}")820 complexity = self._calculate_complexity_regex(code, language)821 822 return complexity823 824 def _count_decision_points(self, node: Node) -> int:825 """Recursively count decision points in AST"""826 count = 0827 828 # Decision point node types across languages829 decision_nodes = {830 'if_statement', 'elif_clause', 'else_clause',831 'for_statement', 'while_statement', 'do_statement',832 'case_statement', 'switch_statement',833 'catch_clause', 'except_clause',834 'conditional_expression', 'ternary_expression',835 'boolean_operator', 'binary_expression'836 }837 838 if node.type in decision_nodes:839 count += 1840 841 # Check for boolean operators (&&, ||)842 if node.type == 'binary_expression':843 if node.text and (b'&&' in node.text or b'||' in node.text or b'and' in node.text or b'or' in node.text):844 count += 1845 846 # Recurse through children847 for child in node.children:848 count += self._count_decision_points(child)849 850 return count851 852 def _calculate_complexity_regex(self, code: str, language: str) -> int:853 """Fallback complexity calculation using regex"""854 complexity = 1855 856 # Common patterns across languages857 patterns = [858 r'\bif\b', r'\belif\b', r'\belse\s+if\b',859 r'\bfor\b', r'\bwhile\b', r'\bdo\b',860 r'\bcase\b', r'\bswitch\b',861 r'\bcatch\b', r'\bexcept\b',862 r'\?.*:', r'&&', r'\|\|',863 r'\band\b', r'\bor\b'864 ]865 866 for pattern in patterns:867 complexity += len(re.findall(pattern, code, re.IGNORECASE))868 869 return complexity870 871 def _detect_language_specific_issues(self, code: str, language: str, lines: List[str]) -> List[CodeIssue]:872 """873 Detect language-specific code smells and anti-patterns874 """875 issues = []876 877 if language.lower() in ['python', 'py']:878 issues.extend(self._detect_python_issues(lines))879 elif language.lower() in ['javascript', 'typescript', 'js', 'ts']:880 issues.extend(self._detect_javascript_issues(lines))881 elif language.lower() in ['java']:882 issues.extend(self._detect_java_issues(lines))883 elif language.lower() in ['go']:884 issues.extend(self._detect_go_issues(lines))885 886 return issues887 888 def _detect_python_issues(self, lines: List[str]) -> List[CodeIssue]:889 """Python-specific code smell detection"""890 issues = []891 892 patterns = [893 (r'from\s+\*\s+import', "Wildcard import", "low", "Import specific names instead of using wildcard imports."),894 (r'\btype\s*\(\s*\w+\s*\)\s*==', "Using type() for type checking", "low", "Use isinstance() instead of type() for type checking."),895 (r'len\s*\(\s*\w+\s*\)\s*==\s*0', "Using len() to check empty", "info", "Use 'if not my_list:' instead of 'if len(my_list) == 0'."),896 (r'\.has_key\s*\(', "Deprecated has_key()", "medium", "Use 'key in dict' instead of dict.has_key(key)."),897 (r'except.*,', "Old-style exception syntax", "medium", "Use 'except Exception as e:' instead of 'except Exception, e:'."),898 ]899 900 for i, line in enumerate(lines, 1):901 for pattern, description, severity, suggestion in patterns:902 if re.search(pattern, line):903 issues.append(CodeIssue(904 severity=severity,905 category="code_quality",906 title=f"Python: {description}",907 description=description,908 line_number=i,909 code_snippet=line.strip(),910 suggestion=suggestion911 ))912 913 return issues914 915 def _detect_javascript_issues(self, lines: List[str]) -> List[CodeIssue]:916 """JavaScript/TypeScript-specific code smell detection"""917 issues = []918 919 patterns = [920 (r'new\s+Array\s*\(\)', "Use array literal []", "info", "Use [] instead of new Array()."),921 (r'new\s+Object\s*\(\)', "Use object literal {}", "info", "Use {} instead of new Object()."),922 (r'\.innerHTML\s*\+=', "Inefficient innerHTML concatenation", "medium", "Use DocumentFragment or insertAdjacentHTML."),923 (r'for\s*\(\s*var\s+\w+\s+in\s+', "for-in loop on array", "medium", "Use for-of or forEach for arrays."),924 (r'setTimeout\s*\([^,]*,\s*0\s*\)', "setTimeout(..., 0) hack", "low", "Consider using Promise.resolve().then() or proper async handling."),925 (r'\$\(.*\)\.\w+\s*\([^)]*\)\.\w+\s*\([^)]*\)\.\w+', "jQuery chain too long", "low", "Long jQuery chains are hard to debug."),926 ]927 928 for i, line in enumerate(lines, 1):929 for pattern, description, severity, suggestion in patterns:930 if re.search(pattern, line):931 issues.append(CodeIssue(932 severity=severity,933 category="code_quality",934 title=f"JavaScript: {description}",935 description=description,936 line_number=i,937 code_snippet=line.strip(),938 suggestion=suggestion939 ))940 941 return issues942 943 def _detect_java_issues(self, lines: List[str]) -> List[CodeIssue]:944 """Java-specific code smell detection"""945 issues = []946 947 patterns = [948 (r'System\.out\.print', "Using System.out.print", "info", "Use proper logging framework instead of System.out."),949 (r'\.printStackTrace\s*\(\)', "Printing stack trace", "medium", "Use proper logging instead of printStackTrace()."),950 (r'new\s+String\s*\(', "Unnecessary String construction", "low", "String literals are automatically interned."),951 (r'\+\s*""\s*\+', "String concatenation with empty string", "low", "Use String.valueOf() for type conversion."),952 ]953 954 for i, line in enumerate(lines, 1):955 for pattern, description, severity, suggestion in patterns:956 if re.search(pattern, line):957 issues.append(CodeIssue(958 severity=severity,959 category="code_quality",960 title=f"Java: {description}",961 description=description,962 line_number=i,963 code_snippet=line.strip(),964 suggestion=suggestion965 ))966 967 return issues968 969 def _detect_go_issues(self, lines: List[str]) -> List[CodeIssue]:970 """Go-specific code smell detection"""971 issues = []972 973 patterns = [974 (r'panic\s*\(', "Using panic()", "high", "Return errors instead of using panic() for recoverable errors."),975 (r'_\s*=.*err', "Ignoring error", "critical", "Always handle errors properly in Go."),976 (r'defer.*Close\(\).*\n.*err\s*:=', "Defer after error check", "medium", "Check errors before deferring Close()."),977 ]978 979 for i, line in enumerate(lines, 1):980 for pattern, description, severity, suggestion in patterns:981 if re.search(pattern, line):982 issues.append(CodeIssue(983 severity=severity,984 category="code_quality",985 title=f"Go: {description}",986 description=description,987 line_number=i,988 code_snippet=line.strip(),989 suggestion=suggestion990 ))991 992 return issues993 994 async def _find_related_code(995 self,996 repo_id: str,997 filename: str998 ) -> List[RelatedCode]:999 """1000 Find related code using graph queries1001 Includes: callers, callees, file dependencies, complexity hotspots1002 """1003 related: List[RelatedCode] = []1004 1005 try:1006 # Find entities in the file1007 file_entities = self.graph_db.find_related_by_file(repo_id, filename, limit=20)1008 logger.info(f"[RELATED_CODE] Found {len(file_entities)} entities in {filename}")1009 for entity in file_entities:1010 related.append(RelatedCode(1011 type="file_entity",1012 file=filename,1013 name=entity["name"],1014 reason=f"{entity['type']} defined in this file",1015 line=entity["line"]1016 ))1017 1018 # Extract function names from entities for deeper analysis1019 function_names = [e["name"] for e in file_entities if e["type"] == "Function"]1020 1021 # For each function, find callers (impact analysis)1022 for func_name in function_names[:5]: # Limit to top 5 functions1023 callers = self.graph_db.find_callers(repo_id, func_name, limit=5)1024 for caller in callers:1025 related.append(RelatedCode(1026 type="caller",1027 file=caller.get("file", "unknown"),1028 name=caller.get("name", "unknown"),1029 reason=f"Calls function '{func_name}'",1030 line=caller.get("line")1031 ))1032 1033 # Find file-level dependencies1034 file_deps = self.graph_db.find_file_dependencies(repo_id, filename)1035 for dep in file_deps[:5]: # Top 5 most coupled files1036 related.append(RelatedCode(1037 type="file_dependency",1038 file=dep.get("file", "unknown"),1039 name=f"{dep.get('call_count', 0)} calls",1040 reason=f"File dependency ({dep.get('call_count', 0)} function calls)",1041 line=None1042 ))1043 1044 # Check for complexity hotspots in this file1045 complexity_issues = self.graph_db.get_complexity_hotspots(repo_id, min_calls=5, limit=10)1046 for hotspot in complexity_issues:1047 if hotspot.get("file") == filename:1048 related.append(RelatedCode(1049 type="complexity_hotspot",1050 file=hotspot.get("file", "unknown"),1051 name=hotspot.get("name", "unknown"),1052 reason=f"Complexity hotspot: {hotspot.get('call_count', 0)} outgoing calls",1053 line=hotspot.get("line")1054 ))1055 1056 logger.info(f"[RELATED_CODE] Returning {len(related)} related code items for {filename}")1057 1058 except Exception as e:1059 logger.error(f"Error finding related code: {e}")1060 1061 return related[:30] # Limit total results1062 1063 def _calculate_file_metrics(self, code: str, language: str) -> Dict[str, Any]:1064 """Calculate basic file metrics"""1065 lines = code.split('\n')1066 1067 return {1068 "total_lines": len(lines),1069 "code_lines": len([l for l in lines if l.strip() and not l.strip().startswith('#')]),1070 "comment_lines": len([l for l in lines if l.strip().startswith('#')]),1071 "blank_lines": len([l for l in lines if not l.strip()]),1072 "language": language1073 }1074 1075 def _parse_patch(self, patch: str) -> List[Dict[str, Any]]:1076 """1077 Parse unified diff patch1078 Returns list of changed lines with metadata1079 """1080 changed_lines: List[Dict[str, Any]] = []1081 1082 for line in patch.split('\n'):1083 if line.startswith('+') and not line.startswith('+++'):1084 changed_lines.append({1085 'type': 'add',1086 'content': line[1:],1087 'line': line1088 })1089 elif line.startswith('-') and not line.startswith('---'):1090 changed_lines.append({1091 'type': 'delete',1092 'content': line[1:],1093 'line': line1094 })1095 1096 return changed_lines1097 1098 def _extract_hunks(self, patch: str) -> List[str]:1099 """Extract hunks from patch"""1100 hunks = []1101 current_hunk = []1102 1103 for line in patch.split('\n'):1104 if line.startswith('@@'):1105 if current_hunk:1106 hunks.append('\n'.join(current_hunk))1107 current_hunk = [line]1108 elif current_hunk:1109 current_hunk.append(line)1110 1111 if current_hunk:1112 hunks.append('\n'.join(current_hunk))1113 1114 return hunks1115 1116 def _analyze_pr_impact(self, repo_id: str, files: List[Any]) -> Dict[str, Any]:1117 """1118 Analyze the impact of PR across the codebase using graph queries1119 Returns metrics about affected areas, coupling, and potential risks1120 """1121 impact_metrics = {1122 "total_affected_callers": 0,1123 "high_coupling_files": [],1124 "complexity_hotspots": [],1125 "unused_code_introduced": [],1126 "cross_file_dependencies": 01127 }1128 1129 try:1130 # Get list of changed files1131 changed_files = [f.filename for f in files if hasattr(f, 'filename')]1132 1133 # Check for highly coupled files in the changeset1134 coupled_files = self.graph_db.get_highly_coupled_files(repo_id, min_connections=5, limit=10)1135 for coupling in coupled_files:1136 if coupling["source_file"] in changed_files or coupling["target_file"] in changed_files:1137 impact_metrics["high_coupling_files"].append({1138 "source": coupling["source_file"],1139 "target": coupling["target_file"],1140 "connections": coupling["connections"]1141 })1142 1143 # Find complexity hotspots in changed files1144 hotspots = self.graph_db.get_complexity_hotspots(repo_id, min_calls=5, limit=10)1145 for hotspot in hotspots:1146 if hotspot["file"] in changed_files:1147 impact_metrics["complexity_hotspots"].append({1148 "file": hotspot["file"],1149 "function": hotspot["name"],1150 "call_count": hotspot["call_count"]1151 })1152 1153 # Count total affected callers (impact radius)1154 for file_path in changed_files[:10]: # Limit to first 10 files1155 entities = self.graph_db.find_related_by_file(repo_id, file_path, limit=20)1156 for entity in entities:1157 if entity["type"] == "Function":1158 callers = self.graph_db.find_callers(repo_id, entity["name"], limit=100)1159 impact_metrics["total_affected_callers"] += len(callers)1160 1161 # Check for unused functions (potential dead code)1162 unused = self.graph_db.find_unused_functions(repo_id, limit=20)1163 for unused_func in unused:1164 if unused_func["file"] in changed_files:1165 impact_metrics["unused_code_introduced"].append({1166 "file": unused_func["file"],1167 "function": unused_func["name"],1168 "line": unused_func["line"]1169 })1170 1171 # Calculate cross-file dependencies1172 for file_path in changed_files:1173 deps = self.graph_db.find_file_dependencies(repo_id, file_path)1174 impact_metrics["cross_file_dependencies"] += len(deps)1175 1176 logger.info(f"PR Impact Analysis: {impact_metrics['total_affected_callers']} affected callers, "1177 f"{len(impact_metrics['high_coupling_files'])} coupled files, "1178 f"{len(impact_metrics['complexity_hotspots'])} hotspots")1179 1180 except Exception as e:1181 logger.error(f"Error analyzing PR impact: {e}")1182 1183 return impact_metrics1184 