Analyzes fullstack codebases for quality issues including:
- Code complexity metrics (cyclomatic complexity, cognitive complexity)
- Security vulnerabilities (hardcoded secrets, injection patterns)
- Dependency health (outdated packages, known vulnerabilities)
- Test coverage estimation
python code_quality_analyzer.py /path/to/project
python code_quality_analyzer.py . --json
python code_quality_analyzer.py /path/to/project --output report.json
from collections import defaultdict
from typing import Dict, List, Optional, Tuple
# File extensions to analyze
FRONTEND_EXTENSIONS = {".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte"}
BACKEND_EXTENSIONS = {".py", ".go", ".java", ".rb", ".php", ".cs"}
CONFIG_EXTENSIONS = {".json", ".yaml", ".yml", ".toml", ".env"}
ALL_CODE_EXTENSIONS = FRONTEND_EXTENSIONS | BACKEND_EXTENSIONS
SKIP_DIRS = {"node_modules", "vendor", ".git", "__pycache__", "dist", "build",
".next", ".venv", "venv", "env", "coverage", ".pytest_cache"}
# Security patterns to detect
"pattern": r"(?:password|secret|api_key|apikey|token|auth)[\s]*[=:][\s]*['\"][^'\"]{8,}['\"]",
"message": "Potential hardcoded secret detected"
"pattern": r"(?:execute|query|raw)\s*\(\s*[f'\"].*\{.*\}|%s|%d|\$\d",
"message": "Potential SQL injection vulnerability"
"pattern": r"innerHTML\s*=|v-html",
"message": "Potential XSS vulnerability - unescaped HTML rendering"
"message": "React unsafe HTML pattern detected - ensure content is sanitized"
"pattern": r"http://(?!localhost|127\.0\.0\.1)",
"message": "Insecure HTTP protocol used"
"pattern": r"console\.log|print\(|debugger|DEBUG\s*=\s*True",
"message": "Debug code should be removed in production"
"pattern": r"(?:TODO|FIXME|HACK|XXX):",
"message": "Unresolved TODO/FIXME comment"
"description": "Function exceeds recommended length",
"description": "Excessive nesting depth",
"description": "File exceeds recommended size",
"pattern": r"(?<![a-zA-Z_])\b(?:[2-9]\d{2,}|\d{4,})\b(?![a-zA-Z_])",
"description": "Magic number should be named constant"
# Dependency vulnerability patterns (simplified - in production use a CVE database)
KNOWN_VULNERABLE_DEPS = {
"lodash": {"vulnerable_below": "4.17.21", "cve": "CVE-2021-23337"},
"axios": {"vulnerable_below": "0.21.2", "cve": "CVE-2021-3749"},
"minimist": {"vulnerable_below": "1.2.6", "cve": "CVE-2021-44906"},
"jsonwebtoken": {"vulnerable_below": "9.0.0", "cve": "CVE-2022-23529"},
def should_skip(path: Path) -> bool:
"""Check if path should be skipped."""
return any(skip in path.parts for skip in SKIP_DIRS)
def count_lines(filepath: Path) -> Tuple[int, int, int]:
"""Count total lines, code lines, and comment lines."""
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
if stripped.startswith(("//", "#", "--", "'")):
return total, code, comments
def calculate_complexity(content: str, language: str) -> Dict:
"""Calculate cyclomatic complexity estimate."""
r"\bif\b", r"\belse\b", r"\belif\b", r"\bfor\b", r"\bwhile\b",
r"\bcase\b", r"\bcatch\b", r"\b\?\b", r"\b&&\b", r"\b\|\|\b",
complexity = 1 # Base complexity
for pattern in decision_patterns:
complexity += len(re.findall(pattern, content, re.IGNORECASE))
max_depth = max(max_depth, current_depth)
current_depth = max(0, current_depth - 1)
"cyclomatic": complexity,
"max_nesting": max_depth,
"rating": "low" if complexity < 10 else "medium" if complexity < 20 else "high"
def analyze_security(filepath: Path, content: str) -> List[Dict]:
"""Scan for security issues."""
lines = content.split("\n")
for pattern_name, pattern_info in SECURITY_PATTERNS.items():
regex = re.compile(pattern_info["pattern"], re.IGNORECASE)
for line_num, line in enumerate(lines, 1):
"severity": pattern_info["severity"],
"message": pattern_info["message"]
def analyze_dependencies(project_path: Path) -> Dict:
"""Analyze project dependencies for issues."""
package_json = project_path / "package.json"
if package_json.exists():
findings["package_managers"].append("npm")
with open(package_json) as f:
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
findings["total_deps"] += len(deps)
for dep, version in deps.items():
# Check against known vulnerabilities
if dep in KNOWN_VULNERABLE_DEPS:
vuln = KNOWN_VULNERABLE_DEPS[dep]
# Simplified version check
clean_version = re.sub(r"[^\d.]", "", version)
if clean_version and clean_version < vuln["vulnerable_below"]:
findings["vulnerable"].append({
"fix_version": vuln["vulnerable_below"],
requirements = project_path / "requirements.txt"
if requirements.exists():
findings["package_managers"].append("pip")
with open(requirements) as f:
lines = [l.strip() for l in f if l.strip() and not l.startswith("#")]
findings["total_deps"] += len(lines)
go_mod = project_path / "go.mod"
findings["package_managers"].append("go")
def analyze_test_coverage(project_path: Path) -> Dict:
"""Estimate test coverage based on file analysis."""
for filepath in project_path.rglob("*"):
if should_skip(filepath) or not filepath.is_file():
if filepath.suffix in ALL_CODE_EXTENSIONS:
name = filepath.stem.lower()
if "test" in name or "spec" in name or "_test" in name:
test_files.append(filepath)
elif not name.startswith("_"):
source_files.append(filepath)
source_count = len(source_files)
test_count = len(test_files)
# Estimate coverage ratio
ratio = min(100, int((test_count / source_count) * 100))
"source_files": source_count,
"test_files": test_count,
"estimated_coverage": ratio,
"rating": "good" if ratio >= 70 else "adequate" if ratio >= 40 else "poor",
"recommendation": None if ratio >= 70 else f"Consider adding more tests ({70 - ratio}% gap to target)"
def analyze_documentation(project_path: Path) -> Dict:
"""Analyze documentation quality."""
"has_contributing": False,
readme_patterns = ["README.md", "README.rst", "README.txt", "readme.md"]
for pattern in readme_patterns:
if (project_path / pattern).exists():
docs["has_readme"] = True
if (project_path / "CONTRIBUTING.md").exists():
docs["has_contributing"] = True
license_patterns = ["LICENSE", "LICENSE.md", "LICENSE.txt"]
for pattern in license_patterns:
if (project_path / pattern).exists():
docs["has_license"] = True
changelog_patterns = ["CHANGELOG.md", "HISTORY.md", "CHANGES.md"]
for pattern in changelog_patterns:
if (project_path / pattern).exists():
docs["has_changelog"] = True
api_doc_dirs = ["docs", "documentation", "api-docs"]
for doc_dir in api_doc_dirs:
doc_path = project_path / doc_dir
docs["api_docs"].append(str(doc_path))
def analyze_project(project_path: Path) -> Dict:
"""Perform full project analysis."""
"languages": defaultdict(lambda: {"files": 0, "lines": 0}),
"high_complexity_files": [],
for filepath in project_path.rglob("*"):
if should_skip(filepath) or not filepath.is_file():
if filepath.suffix not in ALL_CODE_EXTENSIONS:
results["summary"]["files_analyzed"] += 1
total, code, comments = count_lines(filepath)
results["summary"]["total_lines"] += total
results["summary"]["code_lines"] += code
results["summary"]["comment_lines"] += comments
lang = "typescript" if filepath.suffix in {".ts", ".tsx"} else \
"javascript" if filepath.suffix in {".js", ".jsx"} else \
"python" if filepath.suffix == ".py" else \
"go" if filepath.suffix == ".go" else "other"
results["languages"][lang]["files"] += 1
results["languages"][lang]["lines"] += code
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
complexity = calculate_complexity(content, lang)
complexity_scores.append(complexity["cyclomatic"])
if complexity["rating"] == "high":
results["complexity"]["high_complexity_files"].append({
"file": str(filepath.relative_to(project_path)),
"complexity": complexity["cyclomatic"],
"nesting": complexity["max_nesting"]
issues = analyze_security(filepath.relative_to(project_path), content)
security_issues.extend(issues)
if total > CODE_SMELL_PATTERNS["large_file"]["threshold"]:
results["code_smells"].append({
"file": str(filepath.relative_to(project_path)),
"details": f"{total} lines (threshold: {CODE_SMELL_PATTERNS['large_file']['threshold']})"
# Categorize security issues
for issue in security_issues:
severity = issue["severity"]
results["security"][severity].append(issue)
# Calculate average complexity
results["complexity"]["average_complexity"] = round(
sum(complexity_scores) / len(complexity_scores), 1
results["dependencies"] = analyze_dependencies(project_path)
results["tests"] = analyze_test_coverage(project_path)
results["documentation"] = analyze_documentation(project_path)
# Calculate overall score
# Deduct for security issues
score -= len(results["security"]["critical"]) * 15
score -= len(results["security"]["high"]) * 10
score -= len(results["security"]["medium"]) * 5
score -= len(results["security"]["low"]) * 2
# Deduct for high complexity
score -= len(results["complexity"]["high_complexity_files"]) * 3
score -= len(results["code_smells"]) * 2
# Deduct for vulnerable dependencies
score -= len(results["dependencies"].get("vulnerable", [])) * 10
# Deduct for poor test coverage
if results["tests"].get("estimated_coverage", 0) < 50:
elif results["tests"].get("estimated_coverage", 0) < 70:
# Deduct for missing documentation
doc_score = results["documentation"].get("score", 0)
results["overall_score"] = max(0, min(100, score))
"D" if score >= 60 else "F"
# Generate recommendations
results["recommendations"] = generate_recommendations(results)
# Convert defaultdict to regular dict for JSON serialization
results["languages"] = dict(results["languages"])
def generate_recommendations(analysis: Dict) -> List[Dict]:
"""Generate prioritized recommendations."""
# Critical security issues
for issue in analysis["security"]["critical"][:3]:
"issue": issue["message"],
"action": f"Remove or secure sensitive data at line {issue['line']}"
# Vulnerable dependencies
for vuln in analysis["dependencies"].get("vulnerable", [])[:3]:
"issue": f"Vulnerable dependency: {vuln['package']} ({vuln['cve']})",
"action": f"Update to version {vuln['fix_version']} or later"
for issue in analysis["security"]["high"][:3]:
"issue": issue["message"],
"action": "Review and fix security vulnerability"
tests = analysis.get("tests", {})
if tests.get("estimated_coverage", 0) < 50:
"issue": f"Low test coverage: {tests.get('estimated_coverage', 0)}%",
"action": "Add unit tests to improve coverage to at least 70%"
for cplx in analysis["complexity"]["high_complexity_files"][:2]:
"category": "maintainability",
"issue": f"High complexity in {cplx['file']}",
"action": "Refactor to reduce cyclomatic complexity"
docs = analysis.get("documentation", {})
if not docs.get("has_readme"):
"category": "documentation",
"issue": "Missing README.md",
"action": "Add README with project overview and setup instructions"
def print_report(analysis: Dict, verbose: bool = False) -> None:
"""Print human-readable report."""
print("CODE QUALITY ANALYSIS REPORT")
summary = analysis["summary"]
print(f"Overall Score: {analysis['overall_score']}/100 (Grade: {analysis['grade']})")
print(f"Files Analyzed: {summary['files_analyzed']}")
print(f"Total Lines: {summary['total_lines']:,}")
print(f"Code Lines: {summary['code_lines']:,}")
print(f"Comment Lines: {summary['comment_lines']:,}")
print("--- LANGUAGES ---")
for lang, stats in analysis["languages"].items():
print(f" {lang}: {stats['files']} files, {stats['lines']:,} lines")
sec = analysis["security"]
total_sec = sum(len(sec[s]) for s in ["critical", "high", "medium", "low"])
print("--- SECURITY ---")
print(f" Critical: {len(sec['critical'])}")
print(f" High: {len(sec['high'])}")
print(f" Medium: {len(sec['medium'])}")
print(f" Low: {len(sec['low'])}")
if total_sec > 0 and verbose:
for severity in ["critical", "high", "medium"]:
for issue in sec[severity][:3]:
print(f" [{severity.upper()}] {issue['file']}:{issue['line']} - {issue['message']}")
cplx = analysis["complexity"]
print("--- COMPLEXITY ---")
print(f" Average Complexity: {cplx['average_complexity']}")
print(f" High Complexity Files: {len(cplx['high_complexity_files'])}")
deps = analysis["dependencies"]
print("--- DEPENDENCIES ---")
print(f" Package Managers: {', '.join(deps.get('package_managers', ['none']))}")
print(f" Total Dependencies: {deps.get('total_deps', 0)}")
print(f" Vulnerable: {len(deps.get('vulnerable', []))}")
tests = analysis["tests"]
print("--- TEST COVERAGE ---")
print(f" Source Files: {tests.get('source_files', 0)}")
print(f" Test Files: {tests.get('test_files', 0)}")
print(f" Estimated Coverage: {tests.get('estimated_coverage', 0)}% ({tests.get('rating', 'unknown')})")
docs = analysis["documentation"]
print("--- DOCUMENTATION ---")
print(f" README: {'Yes' if docs.get('has_readme') else 'No'}")
print(f" LICENSE: {'Yes' if docs.get('has_license') else 'No'}")
print(f" CONTRIBUTING: {'Yes' if docs.get('has_contributing') else 'No'}")
print(f" CHANGELOG: {'Yes' if docs.get('has_changelog') else 'No'}")
print(f" Score: {docs.get('score', 0)}/100")
if analysis["recommendations"]:
print("--- RECOMMENDATIONS ---")
for i, rec in enumerate(analysis["recommendations"][:10], 1):
print(f"\n{i}. [{rec['priority']}] {rec['category'].upper()}")
print(f" Issue: {rec['issue']}")
print(f" Action: {rec['action']}")
parser = argparse.ArgumentParser(
description="Analyze fullstack codebase for quality issues",
formatter_class=argparse.RawDescriptionHelpFormatter,
%(prog)s /path/to/project
%(prog)s /path/to/project --json --output report.json
help="Path to project directory (default: current directory)"
help="Output in JSON format"
help="Write output to file"
help="Show detailed findings"
args = parser.parse_args()
project_path = Path(args.project_path).resolve()
if not project_path.exists():
print(f"Error: Path does not exist: {project_path}", file=sys.stderr)
analysis = analyze_project(project_path)
output = json.dumps(analysis, indent=2)
with open(args.output, "w") as f:
print(f"Report written to {args.output}")
print_report(analysis, args.verbose)
with open(args.output, "w") as f:
json.dump(analysis, f, indent=2)
print(f"\nDetailed JSON report written to {args.output}")
if __name__ == "__main__":