backend-guard 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. auditor/__init__.py +10 -0
  2. auditor/analyzers/__init__.py +27 -0
  3. auditor/analyzers/frameworks/express/__init__.py +145 -0
  4. auditor/analyzers/frameworks/fastapi/__init__.py +160 -0
  5. auditor/analyzers/frameworks/nestjs/__init__.py +127 -0
  6. auditor/analyzers/languages/javascript/imports.py +45 -0
  7. auditor/analyzers/languages/javascript/parser.py +113 -0
  8. auditor/analyzers/languages/javascript/rules.py +161 -0
  9. auditor/analyzers/languages/python/async_checks.py +82 -0
  10. auditor/analyzers/languages/python/imports.py +55 -0
  11. auditor/analyzers/languages/python/parser.py +45 -0
  12. auditor/analyzers/languages/python/rules.py +197 -0
  13. auditor/analyzers/languages/python/typing.py +78 -0
  14. auditor/analyzers/languages/typescript/imports.py +7 -0
  15. auditor/analyzers/languages/typescript/parser.py +8 -0
  16. auditor/analyzers/languages/typescript/rules.py +75 -0
  17. auditor/analyzers/universal/api_design.py +154 -0
  18. auditor/analyzers/universal/architecture.py +106 -0
  19. auditor/analyzers/universal/complexity.py +190 -0
  20. auditor/analyzers/universal/configuration.py +95 -0
  21. auditor/analyzers/universal/database_design.py +179 -0
  22. auditor/analyzers/universal/dependency.py +94 -0
  23. auditor/analyzers/universal/duplication.py +104 -0
  24. auditor/analyzers/universal/error_handling.py +194 -0
  25. auditor/analyzers/universal/logging.py +156 -0
  26. auditor/analyzers/universal/production_readiness.py +93 -0
  27. auditor/analyzers/universal/security.py +405 -0
  28. auditor/analyzers/universal/solid.py +124 -0
  29. auditor/analyzers/universal/testing.py +136 -0
  30. auditor/baselines.py +55 -0
  31. auditor/cli.py +342 -0
  32. auditor/config.py +186 -0
  33. auditor/core/__init__.py +19 -0
  34. auditor/core/analyzer.py +40 -0
  35. auditor/core/context.py +89 -0
  36. auditor/core/dependency_graph.py +188 -0
  37. auditor/core/findings.py +76 -0
  38. auditor/core/rule.py +134 -0
  39. auditor/core/severity.py +111 -0
  40. auditor/discovery.py +433 -0
  41. auditor/engine.py +146 -0
  42. auditor/integrations/bandit.py +58 -0
  43. auditor/integrations/eslint.py +56 -0
  44. auditor/integrations/external_tools.py +43 -0
  45. auditor/integrations/mypy.py +54 -0
  46. auditor/integrations/pytest.py +43 -0
  47. auditor/integrations/ruff.py +62 -0
  48. auditor/integrations_external/github_actions.py +22 -0
  49. auditor/integrations_external/mcp_server.py +121 -0
  50. auditor/models.py +222 -0
  51. auditor/project.py +37 -0
  52. auditor/registry.py +101 -0
  53. auditor/reporting/json_report.py +15 -0
  54. auditor/reporting/markdown_report.py +123 -0
  55. auditor/reporting/sarif_report.py +87 -0
  56. auditor/reporting/terminal.py +133 -0
  57. auditor/suppressions.py +112 -0
  58. backend_guard-1.0.0.dist-info/METADATA +533 -0
  59. backend_guard-1.0.0.dist-info/RECORD +63 -0
  60. backend_guard-1.0.0.dist-info/WHEEL +5 -0
  61. backend_guard-1.0.0.dist-info/entry_points.txt +3 -0
  62. backend_guard-1.0.0.dist-info/licenses/LICENSE +21 -0
  63. backend_guard-1.0.0.dist-info/top_level.txt +1 -0
auditor/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Universal Backend Codebase Auditor (Backend Guard).
2
+
3
+ A production-grade, extensible static analysis and architecture auditing tool
4
+ for modern backend systems.
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+ __tool_name__ = "backend-guard"
9
+ __project_name__ = "Universal Backend Auditor"
10
+
@@ -0,0 +1,27 @@
1
+ # ruff: noqa: F401, I001
2
+ """Analyzer initialization and dynamic discovery loader."""
3
+
4
+ import auditor.analyzers.universal.architecture
5
+ import auditor.analyzers.universal.solid
6
+ import auditor.analyzers.universal.dependency
7
+ import auditor.analyzers.universal.complexity
8
+ import auditor.analyzers.universal.duplication
9
+ import auditor.analyzers.universal.security
10
+ import auditor.analyzers.universal.error_handling
11
+ import auditor.analyzers.universal.logging
12
+ import auditor.analyzers.universal.configuration
13
+ import auditor.analyzers.universal.database_design
14
+ import auditor.analyzers.universal.api_design
15
+ import auditor.analyzers.universal.testing
16
+ import auditor.analyzers.universal.production_readiness
17
+ import auditor.analyzers.languages.python.rules
18
+ import auditor.analyzers.languages.javascript.rules
19
+ import auditor.analyzers.languages.typescript.rules
20
+ import auditor.analyzers.frameworks.fastapi
21
+ import auditor.analyzers.frameworks.express
22
+ import auditor.analyzers.frameworks.nestjs
23
+
24
+
25
+ def ensure_analyzers_loaded() -> None:
26
+ """Ensure all analyzer and rule modules are imported and registered."""
27
+ pass
@@ -0,0 +1,145 @@
1
+ """Express framework analyzer and rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import List
7
+
8
+ from auditor.analyzers.languages.javascript.parser import parse_js_file
9
+ from auditor.core.analyzer import BaseAnalyzer
10
+ from auditor.core.context import AnalysisContext
11
+ from auditor.core.rule import BaseRule, RuleMetadata
12
+ from auditor.core.severity import Category, Confidence, Severity
13
+ from auditor.models import Finding
14
+ from auditor.registry import register_analyzer, register_rule
15
+
16
+
17
+ @register_rule
18
+ class ExpressMissingErrorMiddlewareRule(BaseRule):
19
+ """Detects Express applications missing standard 4-argument error handling middleware."""
20
+
21
+ def __init__(self) -> None:
22
+ super().__init__(
23
+ RuleMetadata(
24
+ rule_id="EXPRESS-001",
25
+ title="Missing global error handling middleware in Express",
26
+ category=Category.ERROR_HANDLING,
27
+ severity=Severity.HIGH,
28
+ confidence=Confidence.HIGH,
29
+ description="Express server registers routes but contains no 4-argument error-handling middleware (err, req, res, next).",
30
+ impact="Uncaught errors fall back to default HTML error pages, leaking internal stack traces or hanging client requests.",
31
+ recommendation="Register a centralized 4-parameter error handler at the end of the middleware chain.",
32
+ suggested_fix="app.use((err, req, res, next) => { logger.error(err); res.status(500).json({ error: 'Internal Server Error' }); });",
33
+ framework="express",
34
+ language="javascript",
35
+ )
36
+ )
37
+
38
+ def evaluate(self, context: AnalysisContext) -> List[Finding]:
39
+ findings: List[Finding] = []
40
+ js_files = context.get_files_for_language("javascript") + context.get_files_for_language(
41
+ "typescript"
42
+ )
43
+
44
+ has_routes = False
45
+ has_error_middleware = False
46
+ server_entry_file = None
47
+
48
+ for f in js_files:
49
+ data = context.get_js_structure(f) or parse_js_file(f)
50
+ context.set_js_structure(f, data)
51
+
52
+ if data.get("expressRoutes"):
53
+ has_routes = True
54
+ if data.get("expressErrorMiddleware"):
55
+ has_error_middleware = True
56
+
57
+ rel = context.get_relative_path(f).lower()
58
+ if any(
59
+ term in rel
60
+ for term in ["app.js", "server.js", "index.js", "app.ts", "server.ts", "index.ts"]
61
+ ):
62
+ server_entry_file = f
63
+
64
+ if has_routes and not has_error_middleware:
65
+ target_path = server_entry_file or (js_files[0] if js_files else context.root_path)
66
+ findings.append(
67
+ self.create_finding(
68
+ file=context.get_relative_path(target_path),
69
+ line=1,
70
+ description="Express application defines routes but lacks a centralized (err, req, res, next) error handler.",
71
+ evidence="No 4-parameter middleware registered across application files",
72
+ )
73
+ )
74
+ return findings
75
+
76
+
77
+ @register_rule
78
+ class ExpressAsyncRouteWithoutTryCatchRule(BaseRule):
79
+ """Detects async Express route handlers without try/catch or async handler wrapper."""
80
+
81
+ def __init__(self) -> None:
82
+ super().__init__(
83
+ RuleMetadata(
84
+ rule_id="EXPRESS-003",
85
+ title="Async Express route handler without error handling",
86
+ category=Category.ERROR_HANDLING,
87
+ severity=Severity.HIGH,
88
+ confidence=Confidence.HIGH,
89
+ description="Express 4 async route handlers do not catch promise rejections automatically; unhandled errors hang connections indefinitely.",
90
+ impact="A thrown exception inside an async route handler leaves the client request hanging until socket timeout, exhausting server sockets.",
91
+ recommendation="Wrap the async handler with express-async-handler or enclose the body in try/catch calling next(err).",
92
+ suggested_fix="Wrap the handler with asyncHandler(async (req, res) => { ... }) or add try/catch.",
93
+ framework="express",
94
+ language="javascript",
95
+ )
96
+ )
97
+
98
+ def evaluate(self, context: AnalysisContext) -> List[Finding]:
99
+ findings: List[Finding] = []
100
+ js_files = context.get_files_for_language("javascript") + context.get_files_for_language(
101
+ "typescript"
102
+ )
103
+
104
+ for f in js_files:
105
+ lines = context.get_lines(f)
106
+ rel_file = context.get_relative_path(f)
107
+
108
+ for idx, line in enumerate(lines, start=1):
109
+ # Detect async handler registered directly on router or app
110
+ if re.search(
111
+ r"""\b(?:app|router)\.(?:get|post|put|delete|patch)\s*\(\s*['"][^'"]+['"]\s*,\s*async\s*\(""",
112
+ line,
113
+ ):
114
+ # Check next 15 lines for try {
115
+ next_lines = "".join(lines[idx : min(idx + 15, len(lines))])
116
+ if "try" not in next_lines and "asyncHandler" not in line:
117
+ findings.append(
118
+ self.create_finding(
119
+ file=rel_file,
120
+ line=idx,
121
+ description="Async route handler lacks try/catch or asyncHandler wrapper; unhandled rejections will hang requests.",
122
+ evidence=line.strip(),
123
+ )
124
+ )
125
+ return findings
126
+
127
+
128
+ @register_analyzer
129
+ class ExpressFrameworkAnalyzer(BaseAnalyzer):
130
+ name = "express-framework"
131
+ description = "Framework analysis for Express.js: error middleware, async route safety, and middleware ordering"
132
+ framework = "express"
133
+
134
+ def get_rules(self) -> List[BaseRule]:
135
+ return [
136
+ ExpressMissingErrorMiddlewareRule(),
137
+ ExpressAsyncRouteWithoutTryCatchRule(),
138
+ ]
139
+
140
+ def analyze(self, context: AnalysisContext) -> List[Finding]:
141
+ findings: List[Finding] = []
142
+ for rule in self.get_rules():
143
+ if context.config.is_rule_enabled(rule.rule_id):
144
+ findings.extend(rule.evaluate(context))
145
+ return findings
@@ -0,0 +1,160 @@
1
+ """FastAPI framework analyzer and rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from typing import List
7
+
8
+ from auditor.core.analyzer import BaseAnalyzer
9
+ from auditor.core.context import AnalysisContext
10
+ from auditor.core.rule import BaseRule, RuleMetadata
11
+ from auditor.core.severity import Category, Confidence, Severity
12
+ from auditor.models import Finding
13
+ from auditor.registry import register_analyzer, register_rule
14
+
15
+
16
+ @register_rule
17
+ class FastApiRawSqlInRouteRule(BaseRule):
18
+ """Detects direct raw SQL execution inside FastAPI route endpoints."""
19
+
20
+ def __init__(self) -> None:
21
+ super().__init__(
22
+ RuleMetadata(
23
+ rule_id="FASTAPI-001",
24
+ title="Direct raw SQL execution inside route endpoint",
25
+ category=Category.ARCHITECTURE,
26
+ severity=Severity.HIGH,
27
+ confidence=Confidence.HIGH,
28
+ description="FastAPI endpoint directly executes raw SQL query strings instead of delegating to a repository or ORM service.",
29
+ impact="Violates separation of concerns, mixes HTTP transport with database schemas, and makes unit testing endpoints difficult.",
30
+ recommendation="Move database queries into a dedicated repository or service class injected via Depends().",
31
+ suggested_fix="Create a UserRepository and inject it into the route handler via Depends(get_user_repo).",
32
+ framework="fastapi",
33
+ language="python",
34
+ )
35
+ )
36
+
37
+ def evaluate(self, context: AnalysisContext) -> List[Finding]:
38
+ findings: List[Finding] = []
39
+ for py_file in context.get_files_for_language("python"):
40
+ tree = context.get_python_ast(py_file)
41
+ if not tree:
42
+ continue
43
+
44
+ for node in ast.walk(tree):
45
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
46
+ # Check if decorated as router endpoint: @router.get/post/etc.
47
+ is_endpoint = False
48
+ for dec in node.decorator_list:
49
+ if isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
50
+ if dec.func.attr in (
51
+ "get",
52
+ "post",
53
+ "put",
54
+ "delete",
55
+ "patch",
56
+ "api_route",
57
+ ):
58
+ is_endpoint = True
59
+ break
60
+
61
+ if is_endpoint:
62
+ for child in ast.walk(node):
63
+ if isinstance(child, ast.Call) and isinstance(
64
+ child.func, ast.Attribute
65
+ ):
66
+ if child.func.attr in ("execute", "raw"):
67
+ if child.args:
68
+ first_arg = child.args[0]
69
+ is_raw_sql = False
70
+ if isinstance(
71
+ first_arg, (ast.Constant, ast.JoinedStr, ast.BinOp)
72
+ ):
73
+ is_raw_sql = True
74
+ elif isinstance(first_arg, ast.Name):
75
+ # Variable named query or sql, or lookup in function scope
76
+ if any(
77
+ term in first_arg.id.lower()
78
+ for term in ["query", "sql"]
79
+ ):
80
+ is_raw_sql = True
81
+ if is_raw_sql:
82
+ rel_file = context.get_relative_path(py_file)
83
+ findings.append(
84
+ self.create_finding(
85
+ file=rel_file,
86
+ line=child.lineno,
87
+ description=f"Raw SQL execution inside route '{node.name}'.",
88
+ evidence=f"db.{child.func.attr}(...) in @router endpoint",
89
+ )
90
+ )
91
+ return findings
92
+
93
+
94
+ @register_rule
95
+ class FastApiMissingResponseModelRule(BaseRule):
96
+ """Detects mutating route endpoints without response_model or status_code."""
97
+
98
+ def __init__(self) -> None:
99
+ super().__init__(
100
+ RuleMetadata(
101
+ rule_id="FASTAPI-002",
102
+ title="Missing response_model on route endpoint",
103
+ category=Category.API_DESIGN,
104
+ severity=Severity.LOW,
105
+ confidence=Confidence.HIGH,
106
+ description="FastAPI route endpoint does not declare a response_model, bypassing automatic schema filtering and OpenAPI documentation.",
107
+ impact="Internal model fields (like hashed passwords or internal IDs) can be accidentally leaked to clients in responses.",
108
+ recommendation="Explicitly specify response_model=UserResponse in the router decorator.",
109
+ suggested_fix="@router.post('/users', response_model=UserResponse, status_code=201)",
110
+ framework="fastapi",
111
+ language="python",
112
+ )
113
+ )
114
+
115
+ def evaluate(self, context: AnalysisContext) -> List[Finding]:
116
+ findings: List[Finding] = []
117
+ for py_file in context.get_files_for_language("python"):
118
+ tree = context.get_python_ast(py_file)
119
+ if not tree:
120
+ continue
121
+
122
+ for node in ast.walk(tree):
123
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
124
+ for dec in node.decorator_list:
125
+ if isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
126
+ if dec.func.attr in ("post", "put"):
127
+ has_response_model = any(
128
+ kw.arg == "response_model" for kw in dec.keywords
129
+ )
130
+ if not has_response_model:
131
+ rel_file = context.get_relative_path(py_file)
132
+ findings.append(
133
+ self.create_finding(
134
+ file=rel_file,
135
+ line=node.lineno,
136
+ description=f"FastAPI mutating endpoint '{node.name}' has no response_model declared.",
137
+ evidence=f"@{dec.func.attr}(...) without response_model",
138
+ )
139
+ )
140
+ return findings
141
+
142
+
143
+ @register_analyzer
144
+ class FastApiFrameworkAnalyzer(BaseAnalyzer):
145
+ name = "fastapi-framework"
146
+ description = (
147
+ "Framework analysis for FastAPI: route boundaries, response models, and async hygiene"
148
+ )
149
+ framework = "fastapi"
150
+ language = "python"
151
+
152
+ def get_rules(self) -> List[BaseRule]:
153
+ return [FastApiRawSqlInRouteRule(), FastApiMissingResponseModelRule()]
154
+
155
+ def analyze(self, context: AnalysisContext) -> List[Finding]:
156
+ findings: List[Finding] = []
157
+ for rule in self.get_rules():
158
+ if context.config.is_rule_enabled(rule.rule_id):
159
+ findings.extend(rule.evaluate(context))
160
+ return findings
@@ -0,0 +1,127 @@
1
+ """NestJS framework analyzer and rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import List
7
+
8
+ from auditor.core.analyzer import BaseAnalyzer
9
+ from auditor.core.context import AnalysisContext
10
+ from auditor.core.rule import BaseRule, RuleMetadata
11
+ from auditor.core.severity import Category, Confidence, Severity
12
+ from auditor.models import Finding
13
+ from auditor.registry import register_analyzer, register_rule
14
+
15
+
16
+ @register_rule
17
+ class NestJsMissingInjectableRule(BaseRule):
18
+ """Detects service classes in NestJS missing the @Injectable() decorator."""
19
+
20
+ def __init__(self) -> None:
21
+ super().__init__(
22
+ RuleMetadata(
23
+ rule_id="NEST-001",
24
+ title="Service class missing @Injectable() decorator",
25
+ category=Category.PATTERNS,
26
+ severity=Severity.HIGH,
27
+ confidence=Confidence.HIGH,
28
+ description="NestJS service class is not decorated with @Injectable(), preventing the IoC container from managing its lifecycle.",
29
+ impact="NestJS cannot inject dependencies into this class or provide it to controllers, resulting in runtime DI resolution errors.",
30
+ recommendation="Add @Injectable() decorator above the service class declaration.",
31
+ suggested_fix="@Injectable()\nexport class UserService { ... }",
32
+ framework="nestjs",
33
+ language="typescript",
34
+ )
35
+ )
36
+
37
+ def evaluate(self, context: AnalysisContext) -> List[Finding]:
38
+ findings: List[Finding] = []
39
+ ts_files = context.get_files_for_language("typescript")
40
+
41
+ for f in ts_files:
42
+ rel_file = context.get_relative_path(f)
43
+ if not rel_file.endswith(".service.ts"):
44
+ continue
45
+
46
+ lines = context.get_lines(f)
47
+ has_injectable = any("@Injectable()" in line for line in lines)
48
+
49
+ for idx, line in enumerate(lines, start=1):
50
+ if re.search(r"""export\s+class\s+[a-zA-Z0-9_]+Service\b""", line):
51
+ if not has_injectable:
52
+ findings.append(
53
+ self.create_finding(
54
+ file=rel_file,
55
+ line=idx,
56
+ description=f"NestJS service in '{rel_file}' is missing @Injectable() decorator.",
57
+ evidence=line.strip(),
58
+ )
59
+ )
60
+ return findings
61
+
62
+
63
+ @register_rule
64
+ class NestJsDirectDbInControllerRule(BaseRule):
65
+ """Detects direct database queries executed in NestJS @Controller()."""
66
+
67
+ def __init__(self) -> None:
68
+ super().__init__(
69
+ RuleMetadata(
70
+ rule_id="NEST-002",
71
+ title="Direct database access inside NestJS controller",
72
+ category=Category.ARCHITECTURE,
73
+ severity=Severity.HIGH,
74
+ confidence=Confidence.HIGH,
75
+ description="NestJS controller directly accesses database models or ORM repositories rather than delegating to an injected service.",
76
+ impact="Breaks NestJS modular architecture, makes controllers tightly coupled to the database schema, and hampers testing.",
77
+ recommendation="Delegate database operations to an injected service class.",
78
+ suggested_fix="Inject UserService in the constructor and call this.userService.getUserById(id).",
79
+ framework="nestjs",
80
+ language="typescript",
81
+ )
82
+ )
83
+
84
+ def evaluate(self, context: AnalysisContext) -> List[Finding]:
85
+ findings: List[Finding] = []
86
+ ts_files = context.get_files_for_language("typescript")
87
+
88
+ for f in ts_files:
89
+ rel_file = context.get_relative_path(f)
90
+ if not rel_file.endswith(".controller.ts"):
91
+ continue
92
+
93
+ lines = context.get_lines(f)
94
+ for idx, line in enumerate(lines, start=1):
95
+ if re.search(
96
+ r"""\b(?:this\.(?:repository|repo|prisma|dataSource)\.[a-zA-Z0-9_]+\s*\()""",
97
+ line,
98
+ ):
99
+ findings.append(
100
+ self.create_finding(
101
+ file=rel_file,
102
+ line=idx,
103
+ description=f"Direct database / ORM access inside controller in '{rel_file}'.",
104
+ evidence=line.strip(),
105
+ )
106
+ )
107
+ return findings
108
+
109
+
110
+ @register_analyzer
111
+ class NestJsFrameworkAnalyzer(BaseAnalyzer):
112
+ name = "nestjs-framework"
113
+ description = (
114
+ "Framework analysis for NestJS: dependency injection, modules, and controller boundaries"
115
+ )
116
+ framework = "nestjs"
117
+ language = "typescript"
118
+
119
+ def get_rules(self) -> List[BaseRule]:
120
+ return [NestJsMissingInjectableRule(), NestJsDirectDbInControllerRule()]
121
+
122
+ def analyze(self, context: AnalysisContext) -> List[Finding]:
123
+ findings: List[Finding] = []
124
+ for rule in self.get_rules():
125
+ if context.config.is_rule_enabled(rule.rule_id):
126
+ findings.extend(rule.evaluate(context))
127
+ return findings
@@ -0,0 +1,45 @@
1
+ """JavaScript and TypeScript import dependency graph populator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Dict
7
+
8
+ from auditor.analyzers.languages.javascript.parser import parse_js_file
9
+ from auditor.core.context import AnalysisContext
10
+
11
+
12
+ def populate_js_dependency_graph(context: AnalysisContext) -> None:
13
+ """Populate DependencyGraph using parsed JS and TS files."""
14
+ files = context.get_files_for_language("javascript") + context.get_files_for_language(
15
+ "typescript"
16
+ )
17
+
18
+ mod_to_file: Dict[str, Path] = {}
19
+ for f in files:
20
+ rel = context.get_relative_path(f)
21
+ # Normalize without extension
22
+ base_mod = rel
23
+ for ext in (".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"):
24
+ if base_mod.endswith(ext):
25
+ base_mod = base_mod[: -len(ext)]
26
+ break
27
+ mod_to_file[base_mod] = f
28
+ context.dependency_graph.get_or_create_node(base_mod, f, "javascript")
29
+
30
+ for base_mod, f in mod_to_file.items():
31
+ data = context.get_js_structure(f)
32
+ if not data:
33
+ data = parse_js_file(f)
34
+ context.set_js_structure(f, data)
35
+
36
+ for imp in data.get("imports", []):
37
+ mod_target = imp.get("module", "")
38
+ if mod_target.startswith("."):
39
+ # Relative import resolution
40
+ resolved = (f.parent / mod_target).resolve()
41
+ rel_resolved = context.get_relative_path(resolved)
42
+ for candidate_mod in mod_to_file.keys():
43
+ if rel_resolved.startswith(candidate_mod):
44
+ context.dependency_graph.add_dependency(base_mod, candidate_mod)
45
+ break
@@ -0,0 +1,113 @@
1
+ """JavaScript and TypeScript parsing bridge via Node.js helper with fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Any, Dict
10
+
11
+ PARSER_SCRIPT = Path(__file__).parent / "js_ast_parser.js"
12
+
13
+
14
+ def parse_js_file(file_path: Path) -> Dict[str, Any]:
15
+ """Parse JavaScript or TypeScript file into structured AST metadata."""
16
+ node_bin = shutil.which("node")
17
+ if node_bin and PARSER_SCRIPT.is_file():
18
+ try:
19
+ res = subprocess.run(
20
+ [node_bin, str(PARSER_SCRIPT), str(file_path.resolve())],
21
+ capture_output=True,
22
+ text=True,
23
+ timeout=8,
24
+ check=False,
25
+ )
26
+ if res.returncode == 0 and res.stdout.strip():
27
+ return json.loads(res.stdout)
28
+ except Exception:
29
+ pass
30
+
31
+ # High-fidelity fallback parser in pure Python
32
+ return _fallback_js_parser(file_path)
33
+
34
+
35
+ def _fallback_js_parser(file_path: Path) -> Dict[str, Any]:
36
+ """Pure Python fallback parser extracting JS/TS structure."""
37
+ import re
38
+
39
+ result: Dict[str, Any] = {
40
+ "file": str(file_path),
41
+ "totalLines": 0,
42
+ "imports": [],
43
+ "exports": [],
44
+ "functions": [],
45
+ "blockingCalls": [],
46
+ "evalCalls": [],
47
+ "unhandledPromises": [],
48
+ "anyTypes": [],
49
+ "expressRoutes": [],
50
+ "expressErrorMiddleware": [],
51
+ }
52
+
53
+ try:
54
+ lines = file_path.read_text(encoding="utf-8", errors="replace").splitlines()
55
+ result["totalLines"] = len(lines)
56
+ for idx, line in enumerate(lines, start=1):
57
+ trimmed = line.strip()
58
+ if trimmed.startswith("//") or trimmed.startswith("/*") or trimmed.startswith("*"):
59
+ continue
60
+
61
+ # Imports
62
+ esm = re.search(r"""import\s+(?:(?:[\w*\s{},]+)\s+from\s+)?['"]([^'"]+)['"]""", line)
63
+ if esm:
64
+ result["imports"].append({"module": esm.group(1), "line": idx})
65
+ cjs = re.search(r"""require\s*\(\s*['"]([^'"]+)['"]\s*\)""", line)
66
+ if cjs:
67
+ result["imports"].append({"module": cjs.group(1), "line": idx})
68
+
69
+ # Blocking calls
70
+ if re.search(
71
+ r"""\b(?:fs\.readFileSync|fs\.writeFileSync|fs\.existsSync|crypto\.pbkdf2Sync|child_process\.execSync)\b""",
72
+ line,
73
+ ):
74
+ result["blockingCalls"].append({"line": idx, "text": trimmed})
75
+
76
+ # Eval calls
77
+ if re.search(r"""\b(?:eval\s*\(|new\s+Function\s*\()""", line):
78
+ result["evalCalls"].append({"line": idx, "text": trimmed})
79
+
80
+ # Any types
81
+ if re.search(r"""(?::\s*any\b|<any>|as\s+any\b)""", line):
82
+ result["anyTypes"].append({"line": idx, "text": trimmed})
83
+
84
+ # Unhandled promise
85
+ if re.search(r"""\.then\s*\(""", line) and ".catch" not in line:
86
+ result["unhandledPromises"].append({"line": idx, "text": trimmed})
87
+
88
+ # Express routes
89
+ route_m = re.search(
90
+ r"""\b(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(?:async\s*)?(?:\(([^)]*)\)|([a-zA-Z0-9_]+))""",
91
+ line,
92
+ )
93
+ if route_m:
94
+ result["expressRoutes"].append(
95
+ {
96
+ "method": route_m.group(1),
97
+ "path": route_m.group(2),
98
+ "isAsync": "async" in line,
99
+ "line": idx,
100
+ }
101
+ )
102
+
103
+ # Express 4-argument error middleware: (err, req, res, next)
104
+ if re.search(
105
+ r"""\b(?:app|router)\.use\s*\(\s*(?:async\s*)?\(\s*[\w$]+\s*,\s*[\w$]+\s*,\s*[\w$]+\s*,\s*[\w$]+\s*\)""",
106
+ line,
107
+ ):
108
+ result["expressErrorMiddleware"].append({"line": idx, "text": trimmed})
109
+
110
+ except Exception:
111
+ pass
112
+
113
+ return result