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