local-codebase-indexer 0.2.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.
- local_codebase_indexer/__init__.py +3 -0
- local_codebase_indexer/__main__.py +5 -0
- local_codebase_indexer/callgraph.py +279 -0
- local_codebase_indexer/cli.py +122 -0
- local_codebase_indexer/core.py +323 -0
- local_codebase_indexer/cscalls.py +304 -0
- local_codebase_indexer/csdata.py +450 -0
- local_codebase_indexer/dataflow.py +250 -0
- local_codebase_indexer/db.py +194 -0
- local_codebase_indexer/embedder.py +137 -0
- local_codebase_indexer/langparse.py +569 -0
- local_codebase_indexer/mcp_server.py +136 -0
- local_codebase_indexer/query.py +756 -0
- local_codebase_indexer/scanner.py +101 -0
- local_codebase_indexer/sqlaccess.py +301 -0
- local_codebase_indexer/synonyms.py +101 -0
- local_codebase_indexer-0.2.0.dist-info/METADATA +118 -0
- local_codebase_indexer-0.2.0.dist-info/RECORD +21 -0
- local_codebase_indexer-0.2.0.dist-info/WHEEL +5 -0
- local_codebase_indexer-0.2.0.dist-info/entry_points.txt +2 -0
- local_codebase_indexer-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
"""Syntactic layer: tree-sitter parsing into symbols, references and chunks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from tree_sitter import Language, Parser
|
|
9
|
+
|
|
10
|
+
from .csdata import extract_cs_accesses
|
|
11
|
+
from .cscalls import extract_call_sites, member_info
|
|
12
|
+
from .sqlaccess import extract_sql_accesses
|
|
13
|
+
|
|
14
|
+
MAX_CHUNK_LINES = 80
|
|
15
|
+
SPLIT_LINES = 60
|
|
16
|
+
SPLIT_OVERLAP = 8
|
|
17
|
+
WINDOW_LINES = 50
|
|
18
|
+
WINDOW_OVERLAP = 5
|
|
19
|
+
|
|
20
|
+
CLASS_LIKE = {"class", "interface", "struct", "record", "enum", "type"}
|
|
21
|
+
LEAF_TYPES = {"method", "constructor", "destructor", "property", "function", "indexer", "operator", "delegate", "event"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Symbol:
|
|
26
|
+
id: str
|
|
27
|
+
name: str
|
|
28
|
+
qname: str
|
|
29
|
+
type: str
|
|
30
|
+
parent_id: str | None
|
|
31
|
+
start: int
|
|
32
|
+
end: int
|
|
33
|
+
signature: str
|
|
34
|
+
type_name: str = "" # declared / return type (C#), normalised
|
|
35
|
+
min_args: int = -1 # parameter counts for methods and constructors, -1 when not applicable
|
|
36
|
+
max_args: int = -1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Ref:
|
|
41
|
+
kind: str # import | base | ident
|
|
42
|
+
name: str
|
|
43
|
+
line: int
|
|
44
|
+
owner: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Chunk:
|
|
49
|
+
start: int
|
|
50
|
+
end: int
|
|
51
|
+
text: str
|
|
52
|
+
symbol_id: str | None
|
|
53
|
+
header: str # "<type> <qualified name>\n<signature>" used for FTS/embedding
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ParseResult:
|
|
58
|
+
symbols: list[Symbol] = field(default_factory=list)
|
|
59
|
+
refs: list[Ref] = field(default_factory=list)
|
|
60
|
+
chunks: list[Chunk] = field(default_factory=list)
|
|
61
|
+
parsed_with: str = "none"
|
|
62
|
+
accesses: list = field(default_factory=list) # sqlaccess.Access: data access found in SQL and C# code
|
|
63
|
+
call_sites: list = field(default_factory=list) # cscalls.CallSite (C# only)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --------------------------------------------------------------------------- grammars
|
|
67
|
+
|
|
68
|
+
_LANG_CACHE: dict[str, Parser | None] = {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _load_parser(language: str) -> Parser | None:
|
|
72
|
+
if language in _LANG_CACHE:
|
|
73
|
+
return _LANG_CACHE[language]
|
|
74
|
+
parser = None
|
|
75
|
+
try:
|
|
76
|
+
if language == "csharp":
|
|
77
|
+
import tree_sitter_c_sharp as m
|
|
78
|
+
lang = Language(m.language())
|
|
79
|
+
elif language == "python":
|
|
80
|
+
import tree_sitter_python as m
|
|
81
|
+
lang = Language(m.language())
|
|
82
|
+
elif language == "javascript":
|
|
83
|
+
import tree_sitter_javascript as m
|
|
84
|
+
lang = Language(m.language())
|
|
85
|
+
elif language == "typescript":
|
|
86
|
+
import tree_sitter_typescript as m
|
|
87
|
+
lang = Language(m.language_typescript())
|
|
88
|
+
elif language == "tsx":
|
|
89
|
+
import tree_sitter_typescript as m
|
|
90
|
+
lang = Language(m.language_tsx())
|
|
91
|
+
else:
|
|
92
|
+
lang = None
|
|
93
|
+
if lang is not None:
|
|
94
|
+
parser = Parser(lang)
|
|
95
|
+
except Exception: # grammar package missing -> regex fallback
|
|
96
|
+
parser = None
|
|
97
|
+
_LANG_CACHE[language] = parser
|
|
98
|
+
return parser
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --------------------------------------------------------------------------- helpers
|
|
102
|
+
|
|
103
|
+
def normalize_text(text: str) -> str:
|
|
104
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _node_text(node, src: bytes) -> str:
|
|
108
|
+
return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _end_line(node) -> int:
|
|
112
|
+
row, col = node.end_point
|
|
113
|
+
return row if col == 0 and row > node.start_point[0] else row + 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_ATTR_PREFIX = re.compile(r"^(?:\s*\[[^\]]*\]\s*)+")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _signature(node, src: bytes) -> str:
|
|
120
|
+
body = node.child_by_field_name("body")
|
|
121
|
+
end = body.start_byte if body is not None else None
|
|
122
|
+
text = src[node.start_byte:end if end is not None else node.end_byte].decode("utf-8", errors="replace")
|
|
123
|
+
if body is None:
|
|
124
|
+
# properties / fields: cut at first '{' or '=>' or end of first line
|
|
125
|
+
text = re.split(r"\{|=>|\n", text, maxsplit=1)[0]
|
|
126
|
+
text = _ATTR_PREFIX.sub("", text)
|
|
127
|
+
text = " ".join(text.split())
|
|
128
|
+
return text[:300]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _first_identifier(node, src: bytes) -> str | None:
|
|
132
|
+
name = node.child_by_field_name("name")
|
|
133
|
+
if name is not None:
|
|
134
|
+
return _node_text(name, src)
|
|
135
|
+
for ch in node.children:
|
|
136
|
+
if ch.type in ("identifier", "type_identifier", "property_identifier"):
|
|
137
|
+
return _node_text(ch, src)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _field_name_csharp(node, src: bytes) -> str | None:
|
|
142
|
+
for ch in node.children:
|
|
143
|
+
if ch.type == "variable_declaration":
|
|
144
|
+
for d in ch.children:
|
|
145
|
+
if d.type == "variable_declarator":
|
|
146
|
+
return _first_identifier(d, src)
|
|
147
|
+
return _first_identifier(node, src)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
151
|
+
_HERITAGE_SKIP = {"extends", "implements", "where", "new", "class", "struct", "notnull", "unmanaged"}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _heritage_names(text: str) -> list[str]:
|
|
155
|
+
# strip generic arguments, keep the last dotted segment of each base type
|
|
156
|
+
text = re.sub(r"<[^<>]*>", "", text)
|
|
157
|
+
text = re.sub(r"<[^<>]*>", "", text)
|
|
158
|
+
out = []
|
|
159
|
+
for part in text.split(","):
|
|
160
|
+
part = part.split(" where ")[0]
|
|
161
|
+
names = [n for n in _IDENT_RE.findall(part) if n not in _HERITAGE_SKIP]
|
|
162
|
+
if names:
|
|
163
|
+
out.append(names[-1])
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# --------------------------------------------------------------------------- tree-sitter walkers
|
|
168
|
+
|
|
169
|
+
class _Walker:
|
|
170
|
+
def __init__(self, language: str, src: bytes, lines: list[str], path: str):
|
|
171
|
+
self.language = language
|
|
172
|
+
self.src = src
|
|
173
|
+
self.lines = lines
|
|
174
|
+
self.path = path
|
|
175
|
+
self.symbols: list[Symbol] = []
|
|
176
|
+
self.refs: list[Ref] = []
|
|
177
|
+
self.node_symbol: dict[tuple, str] = {}
|
|
178
|
+
self.depth_cap = 80
|
|
179
|
+
|
|
180
|
+
# -- symbol construction
|
|
181
|
+
def add(self, node, name: str, typ: str, parent: Symbol | None, ns: str) -> Symbol:
|
|
182
|
+
prefix = parent.qname if parent else ns
|
|
183
|
+
qname = f"{prefix}.{name}" if prefix else name
|
|
184
|
+
start = node.start_point[0] + 1
|
|
185
|
+
sym = Symbol(
|
|
186
|
+
id=f"{self.path}#{typ}:{qname}@{start}",
|
|
187
|
+
name=name, qname=qname, type=typ,
|
|
188
|
+
parent_id=parent.id if parent else None,
|
|
189
|
+
start=start, end=_end_line(node),
|
|
190
|
+
signature=_signature(node, self.src),
|
|
191
|
+
)
|
|
192
|
+
if self.language == "csharp":
|
|
193
|
+
sym.type_name, sym.min_args, sym.max_args = member_info(node, self.src)
|
|
194
|
+
self.symbols.append(sym)
|
|
195
|
+
self.node_symbol[(node.type, node.start_byte, node.end_byte)] = sym.id
|
|
196
|
+
return sym
|
|
197
|
+
|
|
198
|
+
def add_heritage(self, node, sym: Symbol, text: str) -> None:
|
|
199
|
+
for n in _heritage_names(text):
|
|
200
|
+
self.refs.append(Ref("base", n, node.start_point[0] + 1, sym.id))
|
|
201
|
+
|
|
202
|
+
# -- dispatch
|
|
203
|
+
def walk(self, node, parent: Symbol | None, ns: str, depth: int = 0) -> None:
|
|
204
|
+
if depth > self.depth_cap:
|
|
205
|
+
return
|
|
206
|
+
handler = getattr(self, f"_walk_{self.language}", None)
|
|
207
|
+
children = list(node.children)
|
|
208
|
+
cur_ns = ns
|
|
209
|
+
for ch in children:
|
|
210
|
+
if self.language == "csharp" and ch.type == "file_scoped_namespace_declaration":
|
|
211
|
+
name = ch.child_by_field_name("name")
|
|
212
|
+
cur_ns = _node_text(name, self.src) if name is not None else ns
|
|
213
|
+
self.add(ch, cur_ns, "namespace", None, "")
|
|
214
|
+
continue
|
|
215
|
+
handler(ch, parent, cur_ns, depth)
|
|
216
|
+
|
|
217
|
+
# -- C#
|
|
218
|
+
_CS_CLASS = {
|
|
219
|
+
"class_declaration": "class", "interface_declaration": "interface",
|
|
220
|
+
"struct_declaration": "struct", "record_declaration": "record",
|
|
221
|
+
"record_struct_declaration": "record", "enum_declaration": "enum",
|
|
222
|
+
}
|
|
223
|
+
_CS_MEMBER = {
|
|
224
|
+
"method_declaration": "method", "constructor_declaration": "constructor",
|
|
225
|
+
"destructor_declaration": "destructor", "property_declaration": "property",
|
|
226
|
+
"indexer_declaration": "indexer", "operator_declaration": "operator",
|
|
227
|
+
"delegate_declaration": "delegate", "event_declaration": "event",
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
def _walk_csharp(self, ch, parent, ns, depth):
|
|
231
|
+
t = ch.type
|
|
232
|
+
if t in ("namespace_declaration",):
|
|
233
|
+
name = ch.child_by_field_name("name")
|
|
234
|
+
full = _node_text(name, self.src) if name is not None else ""
|
|
235
|
+
qn = f"{ns}.{full}" if ns and full else (full or ns)
|
|
236
|
+
self.add(ch, qn, "namespace", None, "")
|
|
237
|
+
body = ch.child_by_field_name("body")
|
|
238
|
+
if body is not None:
|
|
239
|
+
self.walk(body, parent, qn, depth + 1)
|
|
240
|
+
elif t in self._CS_CLASS:
|
|
241
|
+
name = _first_identifier(ch, self.src)
|
|
242
|
+
if not name:
|
|
243
|
+
return
|
|
244
|
+
sym = self.add(ch, name, self._CS_CLASS[t], parent, ns)
|
|
245
|
+
for c in ch.children:
|
|
246
|
+
if c.type == "base_list":
|
|
247
|
+
self.add_heritage(c, sym, _node_text(c, self.src).lstrip(": ").strip())
|
|
248
|
+
body = ch.child_by_field_name("body")
|
|
249
|
+
if body is not None:
|
|
250
|
+
self.walk(body, sym, ns, depth + 1)
|
|
251
|
+
else:
|
|
252
|
+
for c in ch.children:
|
|
253
|
+
if c.type in ("declaration_list", "enum_member_declaration_list"):
|
|
254
|
+
self.walk(c, sym, ns, depth + 1)
|
|
255
|
+
elif t in self._CS_MEMBER and parent is not None:
|
|
256
|
+
name = _first_identifier(ch, self.src)
|
|
257
|
+
if t == "indexer_declaration":
|
|
258
|
+
name = "this[]"
|
|
259
|
+
elif t == "operator_declaration":
|
|
260
|
+
name = "operator"
|
|
261
|
+
if name:
|
|
262
|
+
self.add(ch, name, self._CS_MEMBER[t], parent, ns)
|
|
263
|
+
elif t in ("field_declaration", "event_field_declaration") and parent is not None:
|
|
264
|
+
name = _field_name_csharp(ch, self.src)
|
|
265
|
+
if name:
|
|
266
|
+
self.add(ch, name, "field", parent, ns)
|
|
267
|
+
elif t in ("declaration_list", "global_statement", "compilation_unit"):
|
|
268
|
+
self.walk(ch, parent, ns, depth + 1)
|
|
269
|
+
|
|
270
|
+
# -- Python
|
|
271
|
+
def _walk_python(self, ch, parent, ns, depth):
|
|
272
|
+
t = ch.type
|
|
273
|
+
if t == "decorated_definition":
|
|
274
|
+
inner = ch.child_by_field_name("definition")
|
|
275
|
+
if inner is not None:
|
|
276
|
+
self._walk_python_def(inner, ch, parent, ns, depth)
|
|
277
|
+
elif t in ("class_definition", "function_definition"):
|
|
278
|
+
self._walk_python_def(ch, ch, parent, ns, depth)
|
|
279
|
+
|
|
280
|
+
def _walk_python_def(self, node, outer, parent, ns, depth):
|
|
281
|
+
name_node = node.child_by_field_name("name")
|
|
282
|
+
if name_node is None:
|
|
283
|
+
return
|
|
284
|
+
name = _node_text(name_node, self.src)
|
|
285
|
+
if node.type == "class_definition":
|
|
286
|
+
sym = self.add(outer, name, "class", parent, ns)
|
|
287
|
+
sup = node.child_by_field_name("superclasses")
|
|
288
|
+
if sup is not None:
|
|
289
|
+
self.add_heritage(sup, sym, _node_text(sup, self.src).strip("()"))
|
|
290
|
+
body = node.child_by_field_name("body")
|
|
291
|
+
if body is not None:
|
|
292
|
+
self.walk(body, sym, ns, depth + 1)
|
|
293
|
+
elif parent is not None and parent.type == "class":
|
|
294
|
+
self.add(outer, name, "method", parent, ns)
|
|
295
|
+
elif parent is None:
|
|
296
|
+
self.add(outer, name, "function", parent, ns)
|
|
297
|
+
|
|
298
|
+
# -- JavaScript / TypeScript
|
|
299
|
+
_JS_CLASS = {
|
|
300
|
+
"class_declaration": "class", "abstract_class_declaration": "class",
|
|
301
|
+
"interface_declaration": "interface", "enum_declaration": "enum",
|
|
302
|
+
"type_alias_declaration": "type",
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
def _walk_javascript(self, ch, parent, ns, depth):
|
|
306
|
+
t = ch.type
|
|
307
|
+
if t == "export_statement":
|
|
308
|
+
decl = ch.child_by_field_name("declaration")
|
|
309
|
+
if decl is not None:
|
|
310
|
+
self._walk_javascript(decl, parent, ns, depth + 1)
|
|
311
|
+
elif t in self._JS_CLASS:
|
|
312
|
+
name = _first_identifier(ch, self.src)
|
|
313
|
+
if not name:
|
|
314
|
+
return
|
|
315
|
+
sym = self.add(ch, name, self._JS_CLASS[t], parent, ns)
|
|
316
|
+
for c in ch.children:
|
|
317
|
+
if c.type in ("class_heritage", "extends_type_clause", "extends_clause", "implements_clause"):
|
|
318
|
+
self.add_heritage(c, sym, _node_text(c, self.src))
|
|
319
|
+
body = ch.child_by_field_name("body")
|
|
320
|
+
if body is not None:
|
|
321
|
+
self.walk(body, sym, ns, depth + 1)
|
|
322
|
+
elif t in ("function_declaration", "generator_function_declaration") and parent is None:
|
|
323
|
+
name = _first_identifier(ch, self.src)
|
|
324
|
+
if name:
|
|
325
|
+
self.add(ch, name, "function", parent, ns)
|
|
326
|
+
elif t == "method_definition" and parent is not None:
|
|
327
|
+
name = _first_identifier(ch, self.src)
|
|
328
|
+
if name:
|
|
329
|
+
self.add(ch, name, "constructor" if name == "constructor" else "method", parent, ns)
|
|
330
|
+
elif t in ("lexical_declaration", "variable_declaration") and parent is None:
|
|
331
|
+
for d in ch.children:
|
|
332
|
+
if d.type == "variable_declarator":
|
|
333
|
+
val = d.child_by_field_name("value")
|
|
334
|
+
if val is not None and val.type in ("arrow_function", "function_expression", "function"):
|
|
335
|
+
name = _first_identifier(d, self.src)
|
|
336
|
+
if name:
|
|
337
|
+
self.add(ch, name, "function", parent, ns)
|
|
338
|
+
elif t in ("class_body", "statement_block"):
|
|
339
|
+
self.walk(ch, parent, ns, depth + 1)
|
|
340
|
+
|
|
341
|
+
_walk_typescript = _walk_javascript
|
|
342
|
+
_walk_tsx = _walk_javascript
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
_CS_USING = re.compile(r"using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;")
|
|
346
|
+
_PY_IMPORT = re.compile(r"^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w., ]+))", re.M)
|
|
347
|
+
_IDENT_TYPES = {"identifier", "type_identifier", "property_identifier"}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _collect_refs(root, w: _Walker, language: str) -> None:
|
|
351
|
+
"""Iterative walk collecting imports and identifier references per owning symbol."""
|
|
352
|
+
src = w.src
|
|
353
|
+
seen: set[tuple[str, str]] = set()
|
|
354
|
+
stack = [(root, "")]
|
|
355
|
+
while stack:
|
|
356
|
+
node, owner = stack.pop()
|
|
357
|
+
key = (node.type, node.start_byte, node.end_byte)
|
|
358
|
+
sid = w.node_symbol.get(key)
|
|
359
|
+
if sid is not None:
|
|
360
|
+
owner = sid
|
|
361
|
+
t = node.type
|
|
362
|
+
if language == "csharp" and t == "using_directive":
|
|
363
|
+
m = _CS_USING.search(_node_text(node, src))
|
|
364
|
+
if m:
|
|
365
|
+
w.refs.append(Ref("import", m.group(1), node.start_point[0] + 1))
|
|
366
|
+
continue
|
|
367
|
+
if language == "python" and t in ("import_statement", "import_from_statement"):
|
|
368
|
+
for m in _PY_IMPORT.finditer(_node_text(node, src)):
|
|
369
|
+
if m.group(1):
|
|
370
|
+
w.refs.append(Ref("import", m.group(1), node.start_point[0] + 1))
|
|
371
|
+
else:
|
|
372
|
+
for part in m.group(2).split(","):
|
|
373
|
+
part = part.strip().split(" as ")[0].strip()
|
|
374
|
+
if part:
|
|
375
|
+
w.refs.append(Ref("import", part, node.start_point[0] + 1))
|
|
376
|
+
continue
|
|
377
|
+
if language in ("javascript", "typescript", "tsx") and t == "import_statement":
|
|
378
|
+
s = node.child_by_field_name("source")
|
|
379
|
+
if s is not None:
|
|
380
|
+
w.refs.append(Ref("import", _node_text(s, src).strip("'\"` "), node.start_point[0] + 1))
|
|
381
|
+
continue
|
|
382
|
+
if t in _IDENT_TYPES:
|
|
383
|
+
name = _node_text(node, src)
|
|
384
|
+
if len(name) >= 3 and (name, owner) not in seen:
|
|
385
|
+
seen.add((name, owner))
|
|
386
|
+
w.refs.append(Ref("ident", name, node.start_point[0] + 1, owner))
|
|
387
|
+
continue
|
|
388
|
+
for c in node.children:
|
|
389
|
+
stack.append((c, owner))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# --------------------------------------------------------------------------- SQL (regex)
|
|
393
|
+
|
|
394
|
+
_SQL_CREATE = re.compile(
|
|
395
|
+
r"^\s*(?:create\s+(?:or\s+alter\s+)?(proc(?:edure)?|view|function|table)|alter\s+(proc(?:edure)?|view|function))"
|
|
396
|
+
r"\s+([\w\[\]\.\"`]+)",
|
|
397
|
+
re.I | re.M,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _parse_sql(lines: list[str], text: str, path: str) -> list[Symbol]:
|
|
402
|
+
found = []
|
|
403
|
+
for m in _SQL_CREATE.finditer(text):
|
|
404
|
+
line = text.count("\n", 0, m.start()) + 1 + (1 if text[m.start()] == "\n" else 0)
|
|
405
|
+
kind = (m.group(1) or m.group(2)).lower()
|
|
406
|
+
kind = "procedure" if kind.startswith("proc") else kind
|
|
407
|
+
name = m.group(3).replace("[", "").replace("]", "").replace('"', "").replace("`", "")
|
|
408
|
+
found.append((line, kind, name))
|
|
409
|
+
syms = []
|
|
410
|
+
for i, (line, kind, name) in enumerate(found):
|
|
411
|
+
end = (found[i + 1][0] - 1) if i + 1 < len(found) else len(lines)
|
|
412
|
+
syms.append(Symbol(
|
|
413
|
+
id=f"{path}#{kind}:{name}@{line}", name=name.split(".")[-1], qname=name, type=kind,
|
|
414
|
+
parent_id=None, start=line, end=max(line, end),
|
|
415
|
+
signature=" ".join(lines[line - 1].split())[:300],
|
|
416
|
+
))
|
|
417
|
+
return syms
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _sql_file_accesses(lines: list[str], symbols: list[Symbol]) -> list:
|
|
421
|
+
"""Data access per SQL object (each analysed on its own so CTE names and aliases do not leak between
|
|
422
|
+
procedures) plus the lines outside any object (scripts, seed data), which have no source symbol."""
|
|
423
|
+
out = []
|
|
424
|
+
covered = [False] * (len(lines) + 2)
|
|
425
|
+
for s in symbols:
|
|
426
|
+
for i in range(s.start, s.end + 1):
|
|
427
|
+
covered[i] = True
|
|
428
|
+
if s.type == "table":
|
|
429
|
+
continue # a table definition does not read or write anything; trailing seed statements are file level
|
|
430
|
+
for a in extract_sql_accesses("\n".join(lines[s.start - 1:s.end]), s.start):
|
|
431
|
+
a.source_id = s.id
|
|
432
|
+
out.append(a)
|
|
433
|
+
i, n = 1, len(lines)
|
|
434
|
+
while i <= n:
|
|
435
|
+
if covered[i]:
|
|
436
|
+
i += 1
|
|
437
|
+
continue
|
|
438
|
+
j = i
|
|
439
|
+
while j <= n and not covered[j]:
|
|
440
|
+
j += 1
|
|
441
|
+
out.extend(extract_sql_accesses("\n".join(lines[i - 1:j - 1]), i))
|
|
442
|
+
i = j
|
|
443
|
+
for s in symbols:
|
|
444
|
+
if s.type == "table":
|
|
445
|
+
out.extend(extract_sql_accesses("\n".join(lines[s.start - 1:s.end]), s.start)) # file level
|
|
446
|
+
out.sort(key=lambda a: (a.line, a.access, a.key))
|
|
447
|
+
return out
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# --------------------------------------------------------------------------- chunking
|
|
451
|
+
|
|
452
|
+
_COMMENT_PREFIXES = ("//", "///", "#", "*", "/*", "--", "'", "@", "[")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _with_leading_comments(lines: list[str], start: int, floor: int = 1) -> int:
|
|
456
|
+
s = start
|
|
457
|
+
while s - 1 >= floor and start - s < 15:
|
|
458
|
+
prev = lines[s - 2].strip()
|
|
459
|
+
if prev and prev.startswith(_COMMENT_PREFIXES):
|
|
460
|
+
s -= 1
|
|
461
|
+
else:
|
|
462
|
+
break
|
|
463
|
+
return s
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _split_range(start: int, end: int) -> list[tuple[int, int]]:
|
|
467
|
+
if end - start + 1 <= MAX_CHUNK_LINES:
|
|
468
|
+
return [(start, end)]
|
|
469
|
+
out, s = [], start
|
|
470
|
+
while s <= end:
|
|
471
|
+
e = min(end, s + SPLIT_LINES - 1)
|
|
472
|
+
out.append((s, e))
|
|
473
|
+
if e >= end:
|
|
474
|
+
break
|
|
475
|
+
s = e - SPLIT_OVERLAP + 1
|
|
476
|
+
return out
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _make_chunk(lines: list[str], start: int, end: int, sym: Symbol | None, label: str = "") -> Chunk | None:
|
|
480
|
+
text = "\n".join(lines[start - 1:end])
|
|
481
|
+
if not text.strip():
|
|
482
|
+
return None
|
|
483
|
+
if sym is not None:
|
|
484
|
+
header = f"{sym.type} {sym.qname}\n{sym.signature}"
|
|
485
|
+
else:
|
|
486
|
+
header = label
|
|
487
|
+
return Chunk(start, end, text, sym.id if sym else None, header)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def build_chunks(lines: list[str], symbols: list[Symbol], path: str) -> list[Chunk]:
|
|
491
|
+
chunks: list[Chunk] = []
|
|
492
|
+
covered = [False] * (len(lines) + 2)
|
|
493
|
+
|
|
494
|
+
def emit(start, end, sym):
|
|
495
|
+
for s, e in _split_range(start, end):
|
|
496
|
+
c = _make_chunk(lines, s, e, sym)
|
|
497
|
+
if c:
|
|
498
|
+
chunks.append(c)
|
|
499
|
+
for i in range(start, min(end, len(lines)) + 1):
|
|
500
|
+
covered[i] = True
|
|
501
|
+
|
|
502
|
+
children: dict[str, list[Symbol]] = {}
|
|
503
|
+
for s in symbols:
|
|
504
|
+
if s.parent_id:
|
|
505
|
+
children.setdefault(s.parent_id, []).append(s)
|
|
506
|
+
|
|
507
|
+
for s in symbols:
|
|
508
|
+
if s.type in LEAF_TYPES or s.type in ("procedure", "view", "function", "table"):
|
|
509
|
+
emit(_with_leading_comments(lines, s.start), s.end, s)
|
|
510
|
+
elif s.type in CLASS_LIKE:
|
|
511
|
+
members = [c for c in children.get(s.id, []) if c.type in LEAF_TYPES]
|
|
512
|
+
if s.type == "enum" or not members:
|
|
513
|
+
emit(_with_leading_comments(lines, s.start), min(s.end, s.start + MAX_CHUNK_LINES - 1), s)
|
|
514
|
+
else:
|
|
515
|
+
# stop the header before the first member's own leading comments
|
|
516
|
+
first = _with_leading_comments(lines, min(m.start for m in members), s.start + 1)
|
|
517
|
+
emit(_with_leading_comments(lines, s.start), min(max(s.start, first - 1), s.start + 40), s)
|
|
518
|
+
|
|
519
|
+
# uncovered regions (top level scripts, plain text, files without symbols)
|
|
520
|
+
i = 1
|
|
521
|
+
n = len(lines)
|
|
522
|
+
while i <= n:
|
|
523
|
+
if covered[i]:
|
|
524
|
+
i += 1
|
|
525
|
+
continue
|
|
526
|
+
j = i
|
|
527
|
+
while j <= n and not covered[j]:
|
|
528
|
+
j += 1
|
|
529
|
+
seg_start, seg_end = i, j - 1
|
|
530
|
+
if sum(1 for k in range(seg_start, seg_end + 1) if lines[k - 1].strip()) >= (5 if symbols else 1):
|
|
531
|
+
s = seg_start
|
|
532
|
+
while s <= seg_end:
|
|
533
|
+
e = min(seg_end, s + WINDOW_LINES - 1)
|
|
534
|
+
c = _make_chunk(lines, s, e, None, f"text {path}")
|
|
535
|
+
if c:
|
|
536
|
+
chunks.append(c)
|
|
537
|
+
if e >= seg_end:
|
|
538
|
+
break
|
|
539
|
+
s = e - WINDOW_OVERLAP + 1
|
|
540
|
+
i = j
|
|
541
|
+
chunks.sort(key=lambda c: (c.start, c.end))
|
|
542
|
+
return chunks
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# --------------------------------------------------------------------------- public API
|
|
546
|
+
|
|
547
|
+
def parse_file(language: str, text: str, path: str) -> ParseResult:
|
|
548
|
+
text = normalize_text(text)
|
|
549
|
+
lines = text.split("\n")
|
|
550
|
+
result = ParseResult()
|
|
551
|
+
|
|
552
|
+
if language == "sql":
|
|
553
|
+
result.symbols = _parse_sql(lines, text, path)
|
|
554
|
+
result.accesses = _sql_file_accesses(lines, result.symbols)
|
|
555
|
+
result.parsed_with = "regex"
|
|
556
|
+
elif language in ("csharp", "python", "javascript", "typescript", "tsx"):
|
|
557
|
+
parser = _load_parser(language)
|
|
558
|
+
if parser is not None:
|
|
559
|
+
src = text.encode("utf-8")
|
|
560
|
+
tree = parser.parse(src)
|
|
561
|
+
w = _Walker(language, src, lines, path)
|
|
562
|
+
w.walk(tree.root_node, None, "")
|
|
563
|
+
_collect_refs(tree.root_node, w, language)
|
|
564
|
+
result.symbols, result.refs, result.parsed_with = w.symbols, w.refs, "tree-sitter"
|
|
565
|
+
if language == "csharp":
|
|
566
|
+
result.accesses = extract_cs_accesses(tree.root_node, src, w.symbols)
|
|
567
|
+
result.call_sites = extract_call_sites(tree.root_node, src, w.symbols)
|
|
568
|
+
result.chunks = build_chunks(lines, result.symbols, path)
|
|
569
|
+
return result
|