File size: 4,165 Bytes
71d239c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
"""Extract structure from JS / TS files using tree-sitter.

We use `tree-sitter-languages`, which ships prebuilt grammars so the Space needs
no compiler. If tree-sitter is unavailable we fall back to a small regex pass so
the app still works (degraded, but never crashes).
"""
from __future__ import annotations

import re

from schema import Symbol

try:  # prebuilt grammars; no compiler needed on the Space
    from tree_sitter_languages import get_parser
    _HAVE_TS = True
except Exception:  # pragma: no cover - exercised only when dep missing
    _HAVE_TS = False

_LANG_NAME = {"javascript": "javascript", "typescript": "typescript"}


def parse(text: str, lang: str) -> tuple[list[Symbol], list[str]]:
    """Return (symbols, import_targets). Best-effort; never raises."""
    if _HAVE_TS:
        try:
            return _parse_ts(text, lang)
        except Exception:
            pass
    return _parse_regex(text)


# --- tree-sitter path -------------------------------------------------------
def _parse_ts(text: str, lang: str) -> tuple[list[Symbol], list[str]]:
    parser = get_parser(_LANG_NAME.get(lang, "javascript"))
    blob = text.encode("utf-8", "ignore")
    root = parser.parse(blob).root_node
    symbols: list[Symbol] = []
    imports: list[str] = []

    def txt(node) -> str:
        return blob[node.start_byte:node.end_byte].decode("utf-8", "ignore")

    def name_of(node) -> str | None:
        n = node.child_by_field_name("name")
        return txt(n) if n is not None else None

    def visit(node, depth=0):
        t = node.type
        if t in ("function_declaration", "generator_function_declaration"):
            nm = name_of(node)
            if nm:
                symbols.append(Symbol(kind="function", name=nm, lineno=node.start_point[0] + 1))
        elif t in ("class_declaration", "abstract_class_declaration"):
            nm = name_of(node)
            if nm:
                symbols.append(Symbol(kind="class", name=nm, lineno=node.start_point[0] + 1))
        elif t == "lexical_declaration":
            # const Foo = () => {...}  /  const Foo = function...
            for child in node.named_children:
                if child.type == "variable_declarator":
                    val = child.child_by_field_name("value")
                    nm = name_of(child)
                    if nm and val is not None and val.type in (
                            "arrow_function", "function", "function_expression"):
                        symbols.append(Symbol(kind="function", name=nm,
                                              lineno=node.start_point[0] + 1))
        elif t == "import_statement":
            src = node.child_by_field_name("source")
            if src is not None:
                imports.append(txt(src).strip("'\"`"))
        for child in node.named_children:
            visit(child, depth + 1)

    visit(root)
    # require('x') and dynamic import('x') aren't import_statements — sweep them too.
    imports.extend(_REQUIRE_RE.findall(text))
    seen: set[str] = set()
    uniq = [i for i in imports if i and not (i in seen or seen.add(i))]
    return symbols, uniq


# --- regex fallback ---------------------------------------------------------
_FUNC_RE = re.compile(r"\bfunction\s+([A-Za-z_$][\w$]*)")
_ARROW_RE = re.compile(r"\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>")
_CLASS_RE = re.compile(r"\bclass\s+([A-Za-z_$][\w$]*)")
_IMPORT_RE = re.compile(r"""\bimport\b[^'"]*['"]([^'"]+)['"]""")
_REQUIRE_RE = re.compile(r"""\brequire\(\s*['"]([^'"]+)['"]\s*\)""")


def _parse_regex(text: str) -> tuple[list[Symbol], list[str]]:
    symbols: list[Symbol] = []
    for m in _FUNC_RE.finditer(text):
        symbols.append(Symbol(kind="function", name=m.group(1)))
    for m in _ARROW_RE.finditer(text):
        symbols.append(Symbol(kind="function", name=m.group(1)))
    for m in _CLASS_RE.finditer(text):
        symbols.append(Symbol(kind="class", name=m.group(1)))
    imports = _IMPORT_RE.findall(text) + _REQUIRE_RE.findall(text)
    seen: set[str] = set()
    uniq = [i for i in imports if not (i in seen or seen.add(i))]
    return symbols, uniq