CoolFace
Apppublic

salim0986/graph-bug-ai

sourceHugging Facemitupdated 4mo agoView on Hugging Face
0likes
analyzer.py1184 linesDownload Raw Back to src
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