|
1 | 1 | from pathlib import Path |
2 | 2 | import esprima |
3 | 3 | import json |
| 4 | +import re |
4 | 5 |
|
5 | 6 |
|
6 | | -API_METHODS = {"get", "post", "put", "delete", "patch"} |
| 7 | +API_METHODS = {"get", "post", "put", "delete", "patch", "options", "head"} |
| 8 | +ROUTE_OBJECT_KEYWORDS = {"app", "router", "route", "api", "controller", "server"} |
| 9 | +ROUTE_OBJECT_SUFFIXES = ("router", "routes", "route", "app", "server", "controller", "api") |
| 10 | +OPTIONAL_CATCH_PATTERN = re.compile(r'catch\s*(\{)') |
| 11 | +FALLBACK_ENDPOINT_PATTERN = re.compile( |
| 12 | + r'(?P<object>[A-Za-z_$][\w$]*)\s*\.\s*(?P<method>GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\s*\(\s*(?P<route>["\'].*?["\'])?', |
| 13 | + re.IGNORECASE | re.DOTALL |
| 14 | +) |
| 15 | + |
| 16 | + |
| 17 | +def _parse_with_optional_catch_fallback(source, *, loc=True): |
| 18 | + """ |
| 19 | + Attempt to parse JavaScript source. If the parser fails because of |
| 20 | + optional catch binding syntax (catch { ... }), rewrite those blocks |
| 21 | + to catch (__apimesh_err) { ... } and retry once. |
| 22 | + """ |
| 23 | + try: |
| 24 | + return esprima.parseModule(source, loc=loc) |
| 25 | + except Exception as first_error: |
| 26 | + patched_source, replaced = OPTIONAL_CATCH_PATTERN.subn('catch (__apimesh_err) {', source) |
| 27 | + if not replaced: |
| 28 | + raise first_error |
| 29 | + try: |
| 30 | + return esprima.parseModule(patched_source, loc=loc) |
| 31 | + except Exception: |
| 32 | + raise first_error |
| 33 | + |
| 34 | + |
| 35 | +def _extract_endpoints_with_regex(source: str, file_path: Path): |
| 36 | + """Fallback endpoint detector when esprima cannot parse the file.""" |
| 37 | + endpoints = [] |
| 38 | + for match in FALLBACK_ENDPOINT_PATTERN.finditer(source): |
| 39 | + method = match.group('method').upper() |
| 40 | + route_literal = match.group('route') |
| 41 | + route = None |
| 42 | + if route_literal and len(route_literal) >= 2: |
| 43 | + route = route_literal[1:-1] |
| 44 | + start = match.start() |
| 45 | + end = match.end() |
| 46 | + start_line = source.count('\n', 0, start) + 1 |
| 47 | + end_line = source.count('\n', 0, end) + 1 |
| 48 | + obj = match.group('object') or "" |
| 49 | + low = obj.lower() |
| 50 | + if not (low in ROUTE_OBJECT_KEYWORDS or any(low.endswith(suf) for suf in ROUTE_OBJECT_SUFFIXES) or low.startswith(('app', 'api'))): |
| 51 | + continue |
| 52 | + endpoints.append({ |
| 53 | + "type": "function", |
| 54 | + "method": method, |
| 55 | + "route": route, |
| 56 | + "start_line": start_line, |
| 57 | + "end_line": end_line, |
| 58 | + "file_path": str(file_path) |
| 59 | + }) |
| 60 | + return endpoints |
7 | 61 |
|
8 | 62 |
|
9 | 63 | def find_api_endpoints_js(file_path: Path): |
10 | 64 | try: |
11 | 65 | source = file_path.read_text(encoding='utf-8') |
12 | | - tree = esprima.parseModule(source, loc=True) # loc=True gives line numbers |
| 66 | + tree = _parse_with_optional_catch_fallback(source, loc=True) |
13 | 67 | except Exception as e: |
14 | | - print(f"Error parsing {file_path}: {e}") |
15 | | - return [] |
| 68 | + return _extract_endpoints_with_regex(source, file_path) |
16 | 69 |
|
17 | 70 | endpoints = [] |
18 | 71 |
|
|
0 commit comments