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,279 @@
|
|
|
1
|
+
"""Symbol-level call resolution for C#: match every recorded call site to method definitions.
|
|
2
|
+
|
|
3
|
+
Confidence levels
|
|
4
|
+
confirmed the receiver's type is known and exactly one method matches name and argument count
|
|
5
|
+
probable the receiver's type is known but several overloads match, or the call goes through an interface
|
|
6
|
+
that has a single implementation, or the receiver is unknown but the method name is unique
|
|
7
|
+
candidate the receiver's type is unknown and several methods share the name (or an interface has several
|
|
8
|
+
implementations)
|
|
9
|
+
|
|
10
|
+
A receiver whose type is known but is not defined in the index (``List<T>``, ``IDbConnection``, ...) produces no
|
|
11
|
+
edge at all. That is what removes the false matches a name-only search produces. What this cannot do: overload
|
|
12
|
+
resolution by argument type, calls through delegates or lambdas, extension methods, and member access chains
|
|
13
|
+
longer than one call.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sqlite3
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
|
|
21
|
+
CONF_ORDER = {"confirmed": 0, "probable": 1, "candidate": 2}
|
|
22
|
+
_CLASS_KINDS = {"class", "interface", "struct", "record", "enum"}
|
|
23
|
+
_CALLABLE = {"method", "operator"}
|
|
24
|
+
_MAX_UNKNOWN_CANDIDATES = 8
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _Graph:
|
|
28
|
+
def __init__(self, conn: sqlite3.Connection):
|
|
29
|
+
self.sym: dict[str, sqlite3.Row] = {}
|
|
30
|
+
self.types_by_name: dict[str, list] = defaultdict(list)
|
|
31
|
+
self.members: dict[str, dict[str, list]] = defaultdict(lambda: defaultdict(list))
|
|
32
|
+
self.methods_by_name: dict[str, list] = defaultdict(list)
|
|
33
|
+
self.file_ns: dict[str, set[str]] = defaultdict(set)
|
|
34
|
+
for r in conn.execute(
|
|
35
|
+
"SELECT s.id, s.file_path, s.symbol_name, s.symbol_type, s.parent_id, s.qualified_name, s.type_name, "
|
|
36
|
+
"s.min_args, s.max_args FROM symbols s JOIN files f ON f.file_path = s.file_path "
|
|
37
|
+
"WHERE f.language='csharp'"
|
|
38
|
+
):
|
|
39
|
+
self.sym[r["id"]] = r
|
|
40
|
+
t = r["symbol_type"]
|
|
41
|
+
if t in _CLASS_KINDS:
|
|
42
|
+
self.types_by_name[r["symbol_name"]].append(r)
|
|
43
|
+
elif t == "namespace":
|
|
44
|
+
self.file_ns[r["file_path"]].add(r["qualified_name"] or r["symbol_name"])
|
|
45
|
+
if r["parent_id"]:
|
|
46
|
+
self.members[r["parent_id"]][r["symbol_name"]].append(r)
|
|
47
|
+
if t in _CALLABLE:
|
|
48
|
+
self.methods_by_name[r["symbol_name"]].append(r)
|
|
49
|
+
for r in conn.execute(
|
|
50
|
+
"SELECT r.file_path, r.name FROM raw_refs r JOIN files f ON f.file_path = r.file_path "
|
|
51
|
+
"WHERE r.kind='import' AND f.language='csharp'"
|
|
52
|
+
):
|
|
53
|
+
self.file_ns[r["file_path"]].add(r["name"])
|
|
54
|
+
self.bases: dict[str, list[str]] = defaultdict(list)
|
|
55
|
+
for r in conn.execute(
|
|
56
|
+
"SELECT r.owner, r.name FROM raw_refs r WHERE r.kind='base' AND r.owner != ''"
|
|
57
|
+
):
|
|
58
|
+
if r["owner"] in self.sym:
|
|
59
|
+
self.bases[r["owner"]].append(r["name"])
|
|
60
|
+
self._anc: dict[str, list[str]] = {}
|
|
61
|
+
self._desc: dict[str, list[str]] | None = None
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------ type helpers
|
|
64
|
+
def ns_of(self, row) -> str:
|
|
65
|
+
while row["parent_id"] and row["parent_id"] in self.sym:
|
|
66
|
+
row = self.sym[row["parent_id"]]
|
|
67
|
+
q = row["qualified_name"] or ""
|
|
68
|
+
return q.rsplit(".", 1)[0] if "." in q else ""
|
|
69
|
+
|
|
70
|
+
def pick_types(self, name: str, file_path: str) -> list:
|
|
71
|
+
cands = self.types_by_name.get(name, [])
|
|
72
|
+
if len(cands) > 1:
|
|
73
|
+
vis = self.file_ns.get(file_path, set())
|
|
74
|
+
visible = [c for c in cands if self.ns_of(c) in vis]
|
|
75
|
+
if visible:
|
|
76
|
+
return visible
|
|
77
|
+
return cands
|
|
78
|
+
|
|
79
|
+
def ancestors(self, cid: str) -> list[str]:
|
|
80
|
+
if cid in self._anc:
|
|
81
|
+
return self._anc[cid]
|
|
82
|
+
self._anc[cid] = [] # cycle guard
|
|
83
|
+
out, queue, seen = [], list(self.bases.get(cid, [])), {cid}
|
|
84
|
+
file_path = self.sym[cid]["file_path"]
|
|
85
|
+
depth = 0
|
|
86
|
+
while queue and depth < 12:
|
|
87
|
+
nxt = []
|
|
88
|
+
for name in queue:
|
|
89
|
+
for t in self.pick_types(name, file_path):
|
|
90
|
+
if t["id"] not in seen:
|
|
91
|
+
seen.add(t["id"])
|
|
92
|
+
out.append(t["id"])
|
|
93
|
+
nxt.extend(self.bases.get(t["id"], []))
|
|
94
|
+
queue, depth = nxt, depth + 1
|
|
95
|
+
self._anc[cid] = out
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
def implementers(self, iface_id: str) -> list[str]:
|
|
99
|
+
if self._desc is None:
|
|
100
|
+
self._desc = defaultdict(list)
|
|
101
|
+
for cid, row in self.sym.items():
|
|
102
|
+
if row["symbol_type"] in _CLASS_KINDS:
|
|
103
|
+
for a in self.ancestors(cid):
|
|
104
|
+
self._desc[a].append(cid)
|
|
105
|
+
return [c for c in self._desc.get(iface_id, []) if self.sym[c]["symbol_type"] != "interface"]
|
|
106
|
+
|
|
107
|
+
def enclosing_class(self, row):
|
|
108
|
+
while row is not None:
|
|
109
|
+
if row["symbol_type"] in _CLASS_KINDS:
|
|
110
|
+
return row
|
|
111
|
+
row = self.sym.get(row["parent_id"]) if row["parent_id"] else None
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
def member_type(self, cls_id: str, name: str) -> str:
|
|
115
|
+
for c in [cls_id] + self.ancestors(cls_id):
|
|
116
|
+
types = {m["type_name"] for m in self.members[c].get(name, ())
|
|
117
|
+
if m["symbol_type"] in ("field", "property") and m["type_name"]}
|
|
118
|
+
if len(types) == 1:
|
|
119
|
+
return next(iter(types))
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------ method lookup
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _fits(m, argc: int) -> bool:
|
|
125
|
+
return m["min_args"] is None or m["min_args"] < 0 or m["min_args"] <= argc <= m["max_args"]
|
|
126
|
+
|
|
127
|
+
def find_methods(self, class_ids: list[str], name: str, argc: int, ctor: bool = False):
|
|
128
|
+
"""(methods, arity_ok). Searches each class then its ancestors, stopping at the first class that has the name."""
|
|
129
|
+
found: dict[str, object] = {}
|
|
130
|
+
for cid in class_ids:
|
|
131
|
+
chain = [cid] if ctor else [cid] + self.ancestors(cid)
|
|
132
|
+
kinds = ("constructor",) if ctor else tuple(_CALLABLE)
|
|
133
|
+
for c in chain:
|
|
134
|
+
ms = [m for m in self.members[c].get(name, ()) if m["symbol_type"] in kinds]
|
|
135
|
+
if ms:
|
|
136
|
+
for m in ms:
|
|
137
|
+
found[m["id"]] = m
|
|
138
|
+
break
|
|
139
|
+
allm = list(found.values())
|
|
140
|
+
fitting = [m for m in allm if self._fits(m, argc)]
|
|
141
|
+
return (fitting, True) if fitting else (allm, False)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _decode_expr(expr: str):
|
|
145
|
+
kind, name, method, argc, rtype = (expr.split("|") + [""] * 5)[:5]
|
|
146
|
+
return kind, name, method, int(argc or 0), rtype
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def resolve_calls(conn: sqlite3.Connection) -> None:
|
|
150
|
+
"""Rebuild the ``calls`` table from ``call_sites`` and ``symbols``."""
|
|
151
|
+
conn.execute("DELETE FROM calls")
|
|
152
|
+
g = _Graph(conn)
|
|
153
|
+
edges: dict[tuple, tuple] = {}
|
|
154
|
+
|
|
155
|
+
def add(caller, callee, file_path, line, conf, reason):
|
|
156
|
+
key = (caller, callee["id"], line)
|
|
157
|
+
old = edges.get(key)
|
|
158
|
+
if old is None or CONF_ORDER[conf] < CONF_ORDER[old[0]]:
|
|
159
|
+
edges[key] = (conf, reason, file_path)
|
|
160
|
+
|
|
161
|
+
def receiver_classes(site, caller_row, ctx_class, depth: int = 0):
|
|
162
|
+
"""(class rows, how, known). ``known`` is False when the receiver's type could not be determined;
|
|
163
|
+
an empty class list with known=True means the type is external to the index."""
|
|
164
|
+
kind = site["recv_kind"]
|
|
165
|
+
file_path = site["file_path"]
|
|
166
|
+
if kind in ("none", "this"):
|
|
167
|
+
return ([ctx_class], "this" if kind == "this" else "same class", True) if ctx_class is not None else ([], "", False)
|
|
168
|
+
if kind == "base":
|
|
169
|
+
if ctx_class is None:
|
|
170
|
+
return [], "", False
|
|
171
|
+
anc = [g.sym[a] for a in g.ancestors(ctx_class["id"])]
|
|
172
|
+
return anc, "base class", True
|
|
173
|
+
typ = site["recv_type"]
|
|
174
|
+
how = ""
|
|
175
|
+
if kind in ("new", "cast"):
|
|
176
|
+
how = "new/cast type"
|
|
177
|
+
elif kind == "type":
|
|
178
|
+
how = "constructor"
|
|
179
|
+
elif kind in ("name", "member"):
|
|
180
|
+
if typ:
|
|
181
|
+
how = f"local '{site['recv_name']}' is {typ}"
|
|
182
|
+
else:
|
|
183
|
+
if site["recv_expr"] and depth == 0:
|
|
184
|
+
inner = _decode_expr(site["recv_expr"])
|
|
185
|
+
rt = resolve_expr_type(inner, site, ctx_class)
|
|
186
|
+
if rt:
|
|
187
|
+
typ, how = rt, "return type of earlier call"
|
|
188
|
+
if not typ and ctx_class is not None and site["recv_name"]:
|
|
189
|
+
typ = g.member_type(ctx_class["id"], site["recv_name"])
|
|
190
|
+
how = f"'{site['recv_name']}' is declared as {typ}" if typ else ""
|
|
191
|
+
if not typ and kind == "name" and site["recv_name"] in g.types_by_name:
|
|
192
|
+
return g.pick_types(site["recv_name"], file_path), "static call", True
|
|
193
|
+
elif kind == "expr" and site["recv_expr"] and depth == 0:
|
|
194
|
+
rt = resolve_expr_type(_decode_expr(site["recv_expr"]), site, ctx_class)
|
|
195
|
+
if rt:
|
|
196
|
+
typ, how = rt, "return type of chained call"
|
|
197
|
+
if not typ:
|
|
198
|
+
return [], "", False
|
|
199
|
+
classes = g.pick_types(typ, file_path)
|
|
200
|
+
return classes, how, True
|
|
201
|
+
|
|
202
|
+
def resolve_expr_type(inner, site, ctx_class) -> str:
|
|
203
|
+
kind, name, method, argc, rtype = inner
|
|
204
|
+
fake = {"recv_kind": kind, "recv_name": name, "recv_type": rtype, "recv_expr": "", "file_path": site["file_path"]}
|
|
205
|
+
classes, _how, known = receiver_classes(fake, None, ctx_class, depth=1)
|
|
206
|
+
if not known or not classes:
|
|
207
|
+
return ""
|
|
208
|
+
methods, _ok = g.find_methods([c["id"] for c in classes], method, argc)
|
|
209
|
+
types = {m["type_name"] for m in methods if m["type_name"]}
|
|
210
|
+
return next(iter(types)) if len(types) == 1 and len(methods) == 1 else ""
|
|
211
|
+
|
|
212
|
+
sites = conn.execute(
|
|
213
|
+
"SELECT c.* FROM call_sites c JOIN files f ON f.file_path = c.file_path WHERE f.language='csharp'"
|
|
214
|
+
).fetchall()
|
|
215
|
+
for site in sites:
|
|
216
|
+
caller_id = site["caller_id"]
|
|
217
|
+
if not caller_id or caller_id not in g.sym:
|
|
218
|
+
continue
|
|
219
|
+
caller = g.sym[caller_id]
|
|
220
|
+
ctx = g.enclosing_class(caller)
|
|
221
|
+
file_path, line, name, argc = site["file_path"], site["line"], site["name"], site["argc"]
|
|
222
|
+
|
|
223
|
+
if site["is_ctor"]:
|
|
224
|
+
for cls in g.pick_types(name, file_path):
|
|
225
|
+
ctors, ok = g.find_methods([cls["id"]], cls["symbol_name"], argc, ctor=True)
|
|
226
|
+
if len(ctors) == 1:
|
|
227
|
+
add(caller_id, ctors[0], file_path, line, "confirmed" if ok else "probable", "constructor call")
|
|
228
|
+
elif ctors:
|
|
229
|
+
for m in ctors:
|
|
230
|
+
add(caller_id, m, file_path, line, "probable", "constructor overload")
|
|
231
|
+
else:
|
|
232
|
+
add(caller_id, cls, file_path, line, "confirmed", "instantiates class")
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
classes, how, known = receiver_classes(site, caller, ctx)
|
|
236
|
+
if known:
|
|
237
|
+
if not classes:
|
|
238
|
+
continue # type outside the index
|
|
239
|
+
methods, arity_ok = g.find_methods([c["id"] for c in classes], name, argc)
|
|
240
|
+
if not methods:
|
|
241
|
+
continue
|
|
242
|
+
if len(methods) == 1 and arity_ok and len(classes) == 1:
|
|
243
|
+
add(caller_id, methods[0], file_path, line, "confirmed", how)
|
|
244
|
+
else:
|
|
245
|
+
for m in methods:
|
|
246
|
+
add(caller_id, m, file_path, line, "probable",
|
|
247
|
+
f"{how}; {len(methods)} overloads or types match" if how else "several matches")
|
|
248
|
+
for m in methods:
|
|
249
|
+
owner = g.sym.get(m["parent_id"]) if m["parent_id"] else None
|
|
250
|
+
if owner is None or owner["symbol_type"] != "interface":
|
|
251
|
+
continue
|
|
252
|
+
impls = []
|
|
253
|
+
for cid in g.implementers(owner["id"]):
|
|
254
|
+
ims, _ = g.find_methods([cid], name, argc)
|
|
255
|
+
impls.extend(ims)
|
|
256
|
+
if len(impls) == 1:
|
|
257
|
+
add(caller_id, impls[0], file_path, line, "probable",
|
|
258
|
+
f"only implementation of {owner['symbol_name']}.{name}")
|
|
259
|
+
else:
|
|
260
|
+
for im in impls:
|
|
261
|
+
add(caller_id, im, file_path, line, "candidate",
|
|
262
|
+
f"one of {len(impls)} implementations of {owner['symbol_name']}.{name}")
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# receiver type unknown: fall back to the method name, labelled as such
|
|
266
|
+
named = [m for m in g.methods_by_name.get(name, ()) if g._fits(m, argc)]
|
|
267
|
+
if not named or len(named) > _MAX_UNKNOWN_CANDIDATES:
|
|
268
|
+
continue
|
|
269
|
+
if len(named) == 1:
|
|
270
|
+
add(caller_id, named[0], file_path, line, "probable", "receiver type unknown; method name is unique")
|
|
271
|
+
else:
|
|
272
|
+
for m in named:
|
|
273
|
+
add(caller_id, m, file_path, line, "candidate",
|
|
274
|
+
f"receiver type unknown; {len(named)} methods share the name")
|
|
275
|
+
|
|
276
|
+
conn.executemany(
|
|
277
|
+
"INSERT INTO calls(caller_id, callee_id, file_path, line, confidence, reason) VALUES(?,?,?,?,?,?)",
|
|
278
|
+
[(k[0], k[1], v[2], k[2], v[0], v[1]) for k, v in sorted(edges.items(), key=lambda kv: (kv[0][0], kv[0][2], kv[0][1]))],
|
|
279
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Command line interface: ``bin/indexer <tool_name> --params`` (index.md section 4)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _tools(root: str):
|
|
14
|
+
from .query import CodebaseTools
|
|
15
|
+
return CodebaseTools(root)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _emit(data, text: str, as_json: bool) -> None:
|
|
19
|
+
print(json.dumps(data, ensure_ascii=False, indent=2) if as_json else text)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
23
|
+
p = argparse.ArgumentParser(prog="local-codebase-indexer", description="Local codebase indexer & context provider")
|
|
24
|
+
p.add_argument("--root", default=os.environ.get("INDEXER_ROOT", os.getcwd()), help="project root (default: cwd)")
|
|
25
|
+
p.add_argument("--json", action="store_true", help="print JSON instead of compact text")
|
|
26
|
+
p.add_argument("--version", action="version", version=__version__)
|
|
27
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
28
|
+
|
|
29
|
+
s = sub.add_parser("index", help="build / incrementally refresh the index")
|
|
30
|
+
s.add_argument("--force", action="store_true", help="re-index every file")
|
|
31
|
+
|
|
32
|
+
s = sub.add_parser("search", aliases=["codebase_semantic_search"], help="hybrid semantic search")
|
|
33
|
+
s.add_argument("query", nargs="?")
|
|
34
|
+
s.add_argument("--query", dest="query_opt")
|
|
35
|
+
s.add_argument("--limit", type=int, default=5)
|
|
36
|
+
|
|
37
|
+
s = sub.add_parser("relations", aliases=["codebase_get_symbol_relations"], help="symbol dependency graph")
|
|
38
|
+
s.add_argument("symbol", nargs="?")
|
|
39
|
+
s.add_argument("--symbol_name", dest="symbol_opt")
|
|
40
|
+
s.add_argument("--direction", "--relation_direction", dest="direction", default="both",
|
|
41
|
+
choices=["upstream", "downstream", "both"])
|
|
42
|
+
|
|
43
|
+
s = sub.add_parser("context", aliases=["slice", "codebase_get_optimized_context"],
|
|
44
|
+
help="extract only the needed lines of a file")
|
|
45
|
+
s.add_argument("file", nargs="?")
|
|
46
|
+
s.add_argument("--file_path", dest="file_opt")
|
|
47
|
+
s.add_argument("--lines", "--focus_lines", dest="lines", nargs="+", type=int, required=True)
|
|
48
|
+
s.add_argument("--padding", "--padding_lines", dest="padding", type=int, default=15)
|
|
49
|
+
|
|
50
|
+
s = sub.add_parser("data-access", aliases=["dataflow", "codebase_get_data_access"],
|
|
51
|
+
help="who reads / writes / executes a table or stored procedure, and what a procedure touches")
|
|
52
|
+
s.add_argument("object", nargs="?")
|
|
53
|
+
s.add_argument("--object_name", dest="object_opt")
|
|
54
|
+
|
|
55
|
+
sub.add_parser("stats", help="show index statistics")
|
|
56
|
+
sub.add_parser("status", aliases=["codebase_index_status"], help="index health: freshness, failures, consistency")
|
|
57
|
+
sub.add_parser("serve", help="run as MCP server over stdio (JSON-RPC)")
|
|
58
|
+
return p
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv: list[str] | None = None) -> int:
|
|
62
|
+
args = build_parser().parse_args(argv)
|
|
63
|
+
root = os.path.abspath(args.root)
|
|
64
|
+
|
|
65
|
+
if args.cmd == "serve":
|
|
66
|
+
from .mcp_server import serve
|
|
67
|
+
return serve(root)
|
|
68
|
+
|
|
69
|
+
from .query import render_context, render_data_access, render_relations, render_search, render_status
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
t = _tools(root)
|
|
73
|
+
if args.cmd == "index":
|
|
74
|
+
st = t.refresh(force=args.force).as_dict()
|
|
75
|
+
text = (f'indexed {st["scanned"]} files: +{st["added"]} ~{st["updated"]} ={st["unchanged"]} '
|
|
76
|
+
f'-{st["deleted"]} (skipped {st["skipped"]}, failed {st["failed"]}); {st["symbols"]} symbols, {st["chunks"]} chunks')
|
|
77
|
+
if st["warnings"]:
|
|
78
|
+
text += "\nwarnings:\n " + "\n ".join(st["warnings"][:20])
|
|
79
|
+
_emit(st, text, args.json)
|
|
80
|
+
elif args.cmd in ("search", "codebase_semantic_search"):
|
|
81
|
+
q = args.query_opt or args.query
|
|
82
|
+
if not q:
|
|
83
|
+
print("error: a query is required", file=sys.stderr)
|
|
84
|
+
return 2
|
|
85
|
+
res = t.semantic_search(q, args.limit)
|
|
86
|
+
_emit(res, render_search(res), args.json)
|
|
87
|
+
elif args.cmd in ("relations", "codebase_get_symbol_relations"):
|
|
88
|
+
sym = args.symbol_opt or args.symbol
|
|
89
|
+
if not sym:
|
|
90
|
+
print("error: a symbol name is required", file=sys.stderr)
|
|
91
|
+
return 2
|
|
92
|
+
res = t.symbol_relations(sym, args.direction)
|
|
93
|
+
_emit(res, render_relations(res), args.json)
|
|
94
|
+
elif args.cmd in ("context", "slice", "codebase_get_optimized_context"):
|
|
95
|
+
f = args.file_opt or args.file
|
|
96
|
+
if not f:
|
|
97
|
+
print("error: a file path is required", file=sys.stderr)
|
|
98
|
+
return 2
|
|
99
|
+
res = t.optimized_context(f, args.lines, args.padding)
|
|
100
|
+
_emit(res, render_context(res), args.json)
|
|
101
|
+
elif args.cmd in ("data-access", "dataflow", "codebase_get_data_access"):
|
|
102
|
+
name = args.object_opt or args.object
|
|
103
|
+
if not name:
|
|
104
|
+
print("error: an object name is required", file=sys.stderr)
|
|
105
|
+
return 2
|
|
106
|
+
res = t.data_access(name)
|
|
107
|
+
_emit(res, render_data_access(res), args.json)
|
|
108
|
+
elif args.cmd in ("status", "codebase_index_status"):
|
|
109
|
+
res = t.index_status()
|
|
110
|
+
_emit(res, render_status(res), args.json)
|
|
111
|
+
return 0 if res["state"] in ("fresh", "stale") else 3
|
|
112
|
+
elif args.cmd == "stats":
|
|
113
|
+
st = t.stats()
|
|
114
|
+
_emit(st, "\n".join(f"{k}: {v}" for k, v in st.items()), args.json)
|
|
115
|
+
except (ValueError, FileNotFoundError) as exc:
|
|
116
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
117
|
+
return 1
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
sys.exit(main())
|