From a2903f86b6881607fa8ede36875c89a396a0e035 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Tue, 3 Feb 2026 06:57:52 +0000 Subject: [PATCH] Add analyzers module (base, factory, Python, JavaScript) --- vibeguard/analyzers/languages/python.py | 187 ++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 vibeguard/analyzers/languages/python.py diff --git a/vibeguard/analyzers/languages/python.py b/vibeguard/analyzers/languages/python.py new file mode 100644 index 0000000..41db836 --- /dev/null +++ b/vibeguard/analyzers/languages/python.py @@ -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