diff --git a/vibeguard/analyzers/languages/go.py b/vibeguard/analyzers/languages/go.py new file mode 100644 index 0000000..3d5461c --- /dev/null +++ b/vibeguard/analyzers/languages/go.py @@ -0,0 +1,202 @@ +"""Go analyzer for VibeGuard.""" + +import re +from pathlib import Path +from typing import Any + +from vibeguard.analyzers.base import BaseAnalyzer + + +class GoAnalyzer(BaseAnalyzer): + """Analyzer for Go code.""" + + LANGUAGE_NAME = "go" + FILE_EXTENSIONS = [".go"] + + def parse_file(self, path: Path) -> str | None: + """Parse a Go file and return the content.""" + try: + with open(path, "r", encoding="utf-8") as f: + return f.read() + except Exception: + return None + + def analyze(self, content: str, path: Path) -> list[dict[str, Any]]: + """Analyze Go content for anti-patterns.""" + issues: list[dict[str, Any]] = [] + + lines = content.split("\n") + + for i, line in enumerate(lines, 1): + issues.extend(self._check_error_handling(line, i, path)) + issues.extend(self._check_context_usage(line, i, path)) + issues.extend(self._check_goroutine_leak(line, i, path)) + issues.extend(self._check_magic_strings(line, i, path)) + issues.extend(self._check_hardcoded_values(line, i, path)) + + issues.extend(self._check_error_wrapping(content, path)) + issues.extend(self._check_naked_return(path, content)) + + return issues + + def _check_error_handling( + self, line: str, line_num: int, path: Path + ) -> list[dict[str, Any]]: + """Check for improper error handling.""" + issues: list[dict[str, Any]] = [] + + if re.search(r"if\s+err\s*!=\s*nil\s*{[^}]*}\s*$", line): + pass + + if re.search(r"_\s*:=\s*", line) and "err" not in line: + issues.append( + { + "pattern": "IGNORED_ERROR", + "severity": "error", + "file": str(path), + "line": line_num, + "message": "Error being ignored (assigned to blank identifier)", + "suggestion": "Handle the error properly instead of ignoring it", + "language": self.LANGUAGE_NAME, + } + ) + + return issues + + def _check_context_usage( + self, line: str, line_num: int, path: Path + ) -> list[dict[str, Any]]: + """Check for context.Background() misuse.""" + issues: list[dict[str, Any]] = [] + + if re.search(r"context\.Background\(\)", line): + issues.append( + { + "pattern": "CONTEXT_BACKGROUND", + "severity": "warning", + "file": str(path), + "line": line_num, + "message": "context.Background() should not be passed to functions", + "suggestion": "Use context.TODO() or pass request-scoped context", + "language": self.LANGUAGE_NAME, + } + ) + + return issues + + def _check_goroutine_leak( + self, line: str, line_num: int, path: Path + ) -> list[dict[str, Any]]: + """Check for potential goroutine leaks.""" + issues: list[dict[str, Any]] = [] + + if re.search(r"go\s+\w+\(", line): + issues.append( + { + "pattern": "POTENTIAL_GOROUTINE_LEAK", + "severity": "warning", + "file": str(path), + "line": line_num, + "message": "Goroutine started - ensure it won't leak", + "suggestion": "Use wait groups or context to control goroutine lifetime", + "language": self.LANGUAGE_NAME, + } + ) + + return issues + + def _check_magic_strings( + self, line: str, line_num: int, path: Path + ) -> list[dict[str, Any]]: + """Check for magic strings.""" + issues: list[dict[str, Any]] = [] + + if re.search(r'"[^"]{30,}"', line): + issues.append( + { + "pattern": "MAGIC_STRING", + "severity": "warning", + "file": str(path), + "line": line_num, + "message": "Long magic string detected - consider using constants", + "suggestion": "Extract to a named constant", + "language": self.LANGUAGE_NAME, + } + ) + + return issues + + def _check_hardcoded_values( + self, line: str, line_num: int, path: Path + ) -> list[dict[str, Any]]: + """Check for hardcoded values.""" + issues: list[dict[str, Any]] = [] + + if re.search(r"[^0-9a-zA-Z](?:1[0-9]{3}|[2-9][0-9]{3,})(?![0-9])", line): + issues.append( + { + "pattern": "HARDCODED_VALUE", + "severity": "warning", + "file": str(path), + "line": line_num, + "message": "Large hardcoded numeric value detected", + "suggestion": "Extract to a named constant with descriptive name", + "language": self.LANGUAGE_NAME, + } + ) + + return issues + + def _check_error_wrapping(self, content: str, path: Path) -> list[dict[str, Any]]: + """Check for proper error wrapping with %w.""" + issues: list[dict[str, Any]] = [] + + pattern = re.compile(r"fmt\.Errorf\([^)]*%[svx]", re.MULTILINE) + matches = pattern.findall(content) + for match in matches: + if "%w" not in match: + issues.append( + { + "pattern": "ERROR_WRAPPING", + "severity": "warning", + "file": str(path), + "line": 0, + "message": "Error formatting without %w - error won't be wrapped", + "suggestion": "Use %w verb to wrap errors for proper error chains", + "language": self.LANGUAGE_NAME, + } + ) + + return issues + + def _check_naked_return(self, path: Path, content: str) -> list[dict[str, Any]]: + """Check for naked returns in functions with multiple returns.""" + issues: list[dict[str, Any]] = [] + + lines = content.split("\n") + in_func = False + has_multiple_returns = False + + for i, line in enumerate(lines): + if re.match(r"func\s+\([^)]*\)\s*\([^)]*\)\s*{", line): + in_func = True + elif in_func and re.match(r"\s*return\s*$", line): + if has_multiple_returns: + issues.append( + { + "pattern": "NAKED_RETURN", + "severity": "warning", + "file": str(path), + "line": i + 1, + "message": "Naked return in function with multiple return values", + "suggestion": "Use named return values or explicit returns", + "language": self.LANGUAGE_NAME, + } + ) + elif in_func and re.search(r"\breturn\s+\w+", line): + has_multiple_returns = True + elif in_func and line.strip() == "}": + in_func = False + has_multiple_returns = False + + return issues