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.
@@ -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