Add analyzers module (base, factory, Python, JavaScript)
This commit is contained in:
187
vibeguard/analyzers/languages/python.py
Normal file
187
vibeguard/analyzers/languages/python.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
"""Python analyzer for VibeGuard."""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from vibeguard.analyzers.base import BaseAnalyzer
|
||||||
|
|
||||||
|
|
||||||
|
class PythonAnalyzer(BaseAnalyzer):
|
||||||
|
"""Analyzer for Python code using AST."""
|
||||||
|
|
||||||
|
LANGUAGE_NAME = "python"
|
||||||
|
FILE_EXTENSIONS = [".py", ".pyi"]
|
||||||
|
|
||||||
|
def parse_file(self, path: Path) -> ast.AST | None:
|
||||||
|
"""Parse a Python file and return the AST."""
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
return ast.parse(content, filename=str(path))
|
||||||
|
except SyntaxError:
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def analyze(self, tree: ast.AST, path: Path) -> list[dict[str, Any]]:
|
||||||
|
"""Analyze Python AST for anti-patterns."""
|
||||||
|
issues: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Assign):
|
||||||
|
issues.extend(self._check_magic_strings(node, path))
|
||||||
|
issues.extend(self._check_hardcoded_values(node, path))
|
||||||
|
if isinstance(node, ast.FunctionDef):
|
||||||
|
issues.extend(self._check_missing_return_type(node, path))
|
||||||
|
issues.extend(self._check_missing_docstring(node, path))
|
||||||
|
if isinstance(node, ast.ExceptHandler):
|
||||||
|
issues.extend(self._check_bare_except(node, path))
|
||||||
|
if isinstance(node, ast.Compare):
|
||||||
|
issues.extend(self._check_type_check_patterns(node, path))
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def _check_magic_strings(
|
||||||
|
self, node: ast.Assign, path: Path
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Check for magic string assignments."""
|
||||||
|
issues: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Name) and target.id.isupper():
|
||||||
|
if isinstance(node.value, ast.Constant) and isinstance(
|
||||||
|
node.value.value, str
|
||||||
|
):
|
||||||
|
if len(node.value.value) > 20:
|
||||||
|
issues.append(
|
||||||
|
{
|
||||||
|
"pattern": "MAGIC_STRING",
|
||||||
|
"severity": "warning",
|
||||||
|
"file": str(path),
|
||||||
|
"line": node.lineno,
|
||||||
|
"message": "Long magic string detected - consider using constants",
|
||||||
|
"suggestion": "Extract to a named constant at module level",
|
||||||
|
"language": self.LANGUAGE_NAME,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def _check_hardcoded_values(
|
||||||
|
self, node: ast.Assign, path: Path
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Check for hardcoded numeric values."""
|
||||||
|
issues: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for target in node.targets:
|
||||||
|
if isinstance(target, ast.Name):
|
||||||
|
if isinstance(node.value, ast.Constant):
|
||||||
|
value = node.value.value
|
||||||
|
if isinstance(value, (int, float)) and abs(value) > 1000:
|
||||||
|
issues.append(
|
||||||
|
{
|
||||||
|
"pattern": "HARDCODED_VALUE",
|
||||||
|
"severity": "warning",
|
||||||
|
"file": str(path),
|
||||||
|
"line": node.lineno,
|
||||||
|
"message": f"Hardcoded numeric value {value} detected",
|
||||||
|
"suggestion": "Extract to a named constant with descriptive name",
|
||||||
|
"language": self.LANGUAGE_NAME,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def _check_missing_return_type(
|
||||||
|
self, node: ast.FunctionDef, path: Path
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Check for missing return type annotations."""
|
||||||
|
issues: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
if node.returns is None and len(node.body) > 0:
|
||||||
|
issues.append(
|
||||||
|
{
|
||||||
|
"pattern": "MISSING_RETURN_TYPE",
|
||||||
|
"severity": "info",
|
||||||
|
"file": str(path),
|
||||||
|
"line": node.lineno,
|
||||||
|
"message": f"Function '{node.name}' is missing return type annotation",
|
||||||
|
"suggestion": "Add return type annotation for better type safety",
|
||||||
|
"language": self.LANGUAGE_NAME,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def _check_missing_docstring(
|
||||||
|
self, node: ast.FunctionDef, path: Path
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Check for missing function docstrings."""
|
||||||
|
issues: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
has_docstring = (
|
||||||
|
ast.get_docstring(node, clean=False) is not None
|
||||||
|
if node.body
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_docstring and len(node.body) > 1:
|
||||||
|
issues.append(
|
||||||
|
{
|
||||||
|
"pattern": "MISSING_DOCSTRING",
|
||||||
|
"severity": "info",
|
||||||
|
"file": str(path),
|
||||||
|
"line": node.lineno,
|
||||||
|
"message": f"Function '{node.name}' is missing docstring",
|
||||||
|
"suggestion": "Add a docstring explaining the function's purpose and parameters",
|
||||||
|
"language": self.LANGUAGE_NAME,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def _check_bare_except(
|
||||||
|
self, node: ast.ExceptHandler, path: Path
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Check for bare except clauses."""
|
||||||
|
issues: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
if node.type is None:
|
||||||
|
issues.append(
|
||||||
|
{
|
||||||
|
"pattern": "BARE_EXCEPT",
|
||||||
|
"severity": "error",
|
||||||
|
"file": str(path),
|
||||||
|
"line": node.lineno if hasattr(node, "lineno") else 0,
|
||||||
|
"message": "Bare except clause detected - catches all exceptions",
|
||||||
|
"suggestion": "Catch specific exceptions or use 'except Exception'",
|
||||||
|
"language": self.LANGUAGE_NAME,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def _check_type_check_patterns(
|
||||||
|
self, node: ast.Compare, path: Path
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Check for type checking anti-patterns."""
|
||||||
|
issues: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for comparator in node.comparators:
|
||||||
|
if isinstance(comparator, ast.Call):
|
||||||
|
if isinstance(comparator.func, ast.Name):
|
||||||
|
if comparator.func.id == "type":
|
||||||
|
issues.append(
|
||||||
|
{
|
||||||
|
"pattern": "TYPE_CHECK_PATTERN",
|
||||||
|
"severity": "warning",
|
||||||
|
"file": str(path),
|
||||||
|
"line": node.lineno,
|
||||||
|
"message": "Using type() for type checking - use isinstance() instead",
|
||||||
|
"suggestion": "Replace type() == with isinstance() for proper inheritance support",
|
||||||
|
"language": self.LANGUAGE_NAME,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return issues
|
||||||
Reference in New Issue
Block a user