diff --git a/vibeguard/analyzers/languages/javascript.py b/vibeguard/analyzers/languages/javascript.py new file mode 100644 index 0000000..b44e549 --- /dev/null +++ b/vibeguard/analyzers/languages/javascript.py @@ -0,0 +1,206 @@ +"""JavaScript analyzer for VibeGuard.""" + +import re +from pathlib import Path +from typing import Any + +from vibeguard.analyzers.base import BaseAnalyzer + + +class JavaScriptAnalyzer(BaseAnalyzer): + """Analyzer for JavaScript code.""" + + LANGUAGE_NAME = "javascript" + FILE_EXTENSIONS = [".js", ".jsx"] + + MAGIC_STRING_PATTERN = re.compile(r'["\']{3,}[^"\']{20,}["\']{3,}') + HARD_CODED_NUMBER = re.compile(r"[^0-9a-zA-Z](?:1[0-9]{3}|[2-9][0-9]{3,})(?![0-9])") + + def parse_file(self, path: Path) -> str | None: + """Parse a JavaScript 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 JavaScript content for anti-patterns.""" + issues: list[dict[str, Any]] = [] + + lines = content.split("\n") + + for i, line in enumerate(lines, 1): + issues.extend(self._check_console_log(line, i, path)) + issues.extend(self._check_var_usage(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_inconsistent_error_handling(line, i, path)) + issues.extend(self._check_async_await_without_await(line, i, path)) + + issues.extend(self._check_missing_await(content, path)) + issues.extend(self._check_promise_wrapper(path, content)) + + return issues + + def _check_console_log( + self, line: str, line_num: int, path: Path + ) -> list[dict[str, Any]]: + """Check for console.log statements.""" + issues: list[dict[str, Any]] = [] + + if "console.log" in line and "console" not in line.lower(): + issues.append( + { + "pattern": "CONSOLE_LOG", + "severity": "warning", + "file": str(path), + "line": line_num, + "message": "Console.log statement detected - remove or use proper logging", + "suggestion": "Use a logging library instead of console.log", + "language": self.LANGUAGE_NAME, + } + ) + + return issues + + def _check_var_usage( + self, line: str, line_num: int, path: Path + ) -> list[dict[str, Any]]: + """Check for var keyword usage.""" + issues: list[dict[str, Any]] = [] + + if re.search(r"\bvar\s+", line): + issues.append( + { + "pattern": "VAR_KEYWORD", + "severity": "warning", + "file": str(path), + "line": line_num, + "message": "'var' keyword detected - use 'const' or 'let' instead", + "suggestion": "Replace 'var' with 'const' (preferred) or 'let' for block scoping", + "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 self.MAGIC_STRING_PATTERN.search(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 at module level", + "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 numeric values.""" + issues: list[dict[str, Any]] = [] + + if self.HARD_CODED_NUMBER.search(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_inconsistent_error_handling( + self, line: str, line_num: int, path: Path + ) -> list[dict[str, Any]]: + """Check for inconsistent error handling patterns.""" + issues: list[dict[str, Any]] = [] + + if "catch (e)" in line or "catch(err)" in line: + issues.append( + { + "pattern": "INCONSISTENT_ERROR_HANDLING", + "severity": "warning", + "file": str(path), + "line": line_num, + "message": "Generic error variable name - use more descriptive names", + "suggestion": "Use specific error names like 'error', 'err', or 'exception'", + "language": self.LANGUAGE_NAME, + } + ) + + return issues + + def _check_async_await_without_await( + self, line: str, line_num: int, path: Path + ) -> list[dict[str, Any]]: + """Check for async function without await usage.""" + issues: list[dict[str, Any]] = [] + + if re.search(r"async\s+function.*await", line): + pass + elif re.search(r"const\s+\w+\s*=\s*async", line): + pass + elif "await" in line and line.strip().startswith("await"): + pass + + return issues + + def _check_missing_await(self, content: str, path: Path) -> list[dict[str, Any]]: + """Check for potential missing await keywords.""" + issues: list[dict[str, Any]] = [] + + async_pattern = re.compile(r"async\s+\([^)]*\)\s*=>(?!\s*await)") + matches = async_pattern.findall(content) + for match in matches: + issues.append( + { + "pattern": "POTENTIAL_MISSING_AWAIT", + "severity": "info", + "file": str(path), + "line": 0, + "message": "Arrow function with async but no await in body", + "suggestion": "Check if async keyword is needed or if await is missing", + "language": self.LANGUAGE_NAME, + } + ) + + return issues + + def _check_promise_wrapper(self, path: Path, content: str) -> list[dict[str, Any]]: + """Check for unnecessary Promise wrappers.""" + issues: list[dict[str, Any]] = [] + + pattern = re.compile(r"new\s+Promise\s*\(\s*\(\s*resolve\s*\)\s*=>\s*{[^}]*resolve\([^)]+\)") + matches = pattern.findall(content) + for match in matches: + issues.append( + { + "pattern": "UNNECESSARY_PROMISE_WRAPPER", + "severity": "warning", + "file": str(path), + "line": 0, + "message": "Unnecessary Promise wrapper detected", + "suggestion": "Return the value directly instead of wrapping in Promise", + "language": self.LANGUAGE_NAME, + } + ) + + return issues