203 lines
7.2 KiB
Python
203 lines
7.2 KiB
Python
"""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
|