codespine 0.1.7__tar.gz → 0.1.8__tar.gz
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.
- {codespine-0.1.7 → codespine-0.1.8}/PKG-INFO +1 -1
- {codespine-0.1.7 → codespine-0.1.8}/codespine/__init__.py +1 -1
- {codespine-0.1.7 → codespine-0.1.8}/codespine/indexer/call_resolver.py +13 -6
- {codespine-0.1.7 → codespine-0.1.8}/codespine/indexer/engine.py +78 -30
- codespine-0.1.8/codespine/indexer/symbol_builder.py +35 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine.egg-info/PKG-INFO +1 -1
- {codespine-0.1.7 → codespine-0.1.8}/codespine.egg-info/SOURCES.txt +1 -0
- {codespine-0.1.7 → codespine-0.1.8}/pyproject.toml +1 -1
- codespine-0.1.8/tests/test_call_resolver.py +43 -0
- codespine-0.1.8/tests/test_multimodule_index.py +55 -0
- codespine-0.1.7/codespine/indexer/symbol_builder.py +0 -32
- codespine-0.1.7/tests/test_call_resolver.py +0 -30
- {codespine-0.1.7 → codespine-0.1.8}/LICENSE +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/README.md +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/analysis/__init__.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/analysis/community.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/analysis/context.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/analysis/coupling.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/analysis/deadcode.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/analysis/flow.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/analysis/impact.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/cli.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/config.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/db/__init__.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/db/schema.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/db/store.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/diff/__init__.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/diff/branch_diff.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/indexer/__init__.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/indexer/java_parser.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/mcp/__init__.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/mcp/server.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/noise/__init__.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/noise/blocklist.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/search/__init__.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/search/bm25.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/search/fuzzy.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/search/hybrid.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/search/rrf.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/search/vector.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/watch/__init__.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine/watch/watcher.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine.egg-info/dependency_links.txt +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine.egg-info/entry_points.txt +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine.egg-info/requires.txt +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/codespine.egg-info/top_level.txt +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/gindex.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/setup.cfg +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/tests/test_branch_diff_normalize.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/tests/test_index_and_hybrid.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/tests/test_java_parser.py +0 -0
- {codespine-0.1.7 → codespine-0.1.8}/tests/test_search_ranking.py +0 -0
|
@@ -62,16 +62,23 @@ def resolve_calls(
|
|
|
62
62
|
Yields tuples: (source_method_id, target_method_id, confidence, reason)
|
|
63
63
|
"""
|
|
64
64
|
name_arity_to_method_ids: dict[tuple[str, int], list[str]] = defaultdict(list)
|
|
65
|
-
|
|
65
|
+
class_method_index_by_id: dict[str, dict[tuple[str, int], list[str]]] = defaultdict(lambda: defaultdict(list))
|
|
66
|
+
class_method_index_by_fqcn: dict[str, dict[tuple[str, int], list[str]]] = defaultdict(lambda: defaultdict(list))
|
|
66
67
|
for method_id, meta in method_catalog.items():
|
|
67
68
|
key = (meta["name"], int(meta["param_count"]))
|
|
68
69
|
name_arity_to_method_ids[key].append(method_id)
|
|
69
|
-
|
|
70
|
+
class_id = meta.get("class_id", "")
|
|
71
|
+
class_fqcn = meta.get("class_fqcn", "")
|
|
72
|
+
if class_id:
|
|
73
|
+
class_method_index_by_id[class_id][key].append(method_id)
|
|
74
|
+
if class_fqcn:
|
|
75
|
+
class_method_index_by_fqcn[class_fqcn][key].append(method_id)
|
|
70
76
|
|
|
71
77
|
for source_id, call_sites in calls.items():
|
|
72
78
|
src_meta = method_catalog.get(source_id, {})
|
|
73
79
|
src_ctx = method_context.get(source_id, {})
|
|
74
|
-
|
|
80
|
+
src_class_id = src_meta.get("class_id", "") or src_ctx.get("class_id", "")
|
|
81
|
+
src_class_fqcn = src_meta.get("class_fqcn", "")
|
|
75
82
|
local_types = src_ctx.get("local_types", {}) or {}
|
|
76
83
|
field_types = src_ctx.get("field_types", {}) or {}
|
|
77
84
|
|
|
@@ -90,7 +97,7 @@ def resolve_calls(
|
|
|
90
97
|
receiver_type = None
|
|
91
98
|
receiver_is_this = False
|
|
92
99
|
if receiver == "this":
|
|
93
|
-
receiver_type =
|
|
100
|
+
receiver_type = src_class_fqcn
|
|
94
101
|
receiver_is_this = True
|
|
95
102
|
elif receiver in local_types:
|
|
96
103
|
receiver_type = local_types[receiver]
|
|
@@ -102,14 +109,14 @@ def resolve_calls(
|
|
|
102
109
|
receiver_fqcn_candidates = _resolve_type_candidates(receiver_type, src_ctx, class_catalog)
|
|
103
110
|
|
|
104
111
|
for fqcn in receiver_fqcn_candidates:
|
|
105
|
-
targets.extend(
|
|
112
|
+
targets.extend(class_method_index_by_fqcn.get(fqcn, {}).get(key, []))
|
|
106
113
|
|
|
107
114
|
if targets:
|
|
108
115
|
confidence = 1.0 if receiver_is_this else 0.8
|
|
109
116
|
reason = "receiver_this_exact" if receiver_is_this else "receiver_method_match"
|
|
110
117
|
|
|
111
118
|
if not targets:
|
|
112
|
-
in_class =
|
|
119
|
+
in_class = class_method_index_by_id.get(src_class_id, {}).get(key, [])
|
|
113
120
|
if in_class:
|
|
114
121
|
targets = in_class
|
|
115
122
|
confidence = 1.0
|
|
@@ -70,6 +70,7 @@ class JavaIndexer:
|
|
|
70
70
|
method_calls: dict[str, list] = {}
|
|
71
71
|
method_context: dict[str, dict] = {}
|
|
72
72
|
class_catalog: dict[str, list[str]] = self._existing_class_catalog(project_id) if not full else {}
|
|
73
|
+
fqcn_to_class_ids: dict[str, list[str]] = self._existing_class_ids_by_fqcn(project_id) if not full else {}
|
|
73
74
|
class_meta: dict[str, dict] = {}
|
|
74
75
|
class_methods: dict[str, dict[str, str]] = self._existing_class_methods(project_id) if not full else {}
|
|
75
76
|
|
|
@@ -84,6 +85,7 @@ class JavaIndexer:
|
|
|
84
85
|
for file_path in to_reindex:
|
|
85
86
|
rel_path = os.path.relpath(file_path, root_path)
|
|
86
87
|
is_test = "src/test/java" in file_path.replace("\\", "/")
|
|
88
|
+
scope = self._scope_from_rel_path(rel_path)
|
|
87
89
|
|
|
88
90
|
with open(file_path, "rb") as f:
|
|
89
91
|
source = f.read()
|
|
@@ -96,20 +98,25 @@ class JavaIndexer:
|
|
|
96
98
|
self.store.upsert_file(f_id, file_path, project_id, is_test, digest_bytes(source))
|
|
97
99
|
|
|
98
100
|
for cls in parsed.classes:
|
|
99
|
-
c_id = class_id(cls.fqcn)
|
|
101
|
+
c_id = class_id(cls.fqcn, scope)
|
|
100
102
|
self.store.upsert_class(c_id, cls.fqcn, cls.name, cls.package, f_id)
|
|
101
103
|
class_catalog.setdefault(cls.name, [])
|
|
102
104
|
if cls.fqcn not in class_catalog[cls.name]:
|
|
103
105
|
class_catalog[cls.name].append(cls.fqcn)
|
|
104
|
-
|
|
106
|
+
fqcn_to_class_ids.setdefault(cls.fqcn, [])
|
|
107
|
+
if c_id not in fqcn_to_class_ids[cls.fqcn]:
|
|
108
|
+
fqcn_to_class_ids[cls.fqcn].append(c_id)
|
|
109
|
+
class_meta[c_id] = {
|
|
110
|
+
"fqcn": cls.fqcn,
|
|
105
111
|
"package": parsed.package,
|
|
106
112
|
"imports": parsed.imports,
|
|
107
113
|
"extends": cls.extends,
|
|
108
114
|
"interfaces": cls.interfaces,
|
|
115
|
+
"scope": scope,
|
|
109
116
|
}
|
|
110
|
-
class_methods.setdefault(
|
|
117
|
+
class_methods.setdefault(c_id, {})
|
|
111
118
|
|
|
112
|
-
cls_symbol_id = symbol_id("class", cls.fqcn)
|
|
119
|
+
cls_symbol_id = symbol_id("class", cls.fqcn, scope)
|
|
113
120
|
self.store.upsert_symbol(
|
|
114
121
|
symbol_id=cls_symbol_id,
|
|
115
122
|
kind="class",
|
|
@@ -123,7 +130,7 @@ class JavaIndexer:
|
|
|
123
130
|
classes_indexed += 1
|
|
124
131
|
|
|
125
132
|
for method in cls.methods:
|
|
126
|
-
m_id = method_id(cls.fqcn, method.signature)
|
|
133
|
+
m_id = method_id(cls.fqcn, method.signature, scope)
|
|
127
134
|
self.store.upsert_method(
|
|
128
135
|
method_id=m_id,
|
|
129
136
|
class_id=c_id,
|
|
@@ -136,7 +143,7 @@ class JavaIndexer:
|
|
|
136
143
|
)
|
|
137
144
|
|
|
138
145
|
fqname = f"{cls.fqcn}#{method.signature}"
|
|
139
|
-
m_symbol_id = symbol_id("method", fqname)
|
|
146
|
+
m_symbol_id = symbol_id("method", fqname, scope)
|
|
140
147
|
self.store.upsert_symbol(
|
|
141
148
|
symbol_id=m_symbol_id,
|
|
142
149
|
kind="method",
|
|
@@ -154,16 +161,18 @@ class JavaIndexer:
|
|
|
154
161
|
"name": method.name,
|
|
155
162
|
"param_count": len(method.parameter_types),
|
|
156
163
|
"class_fqcn": cls.fqcn,
|
|
164
|
+
"class_id": c_id,
|
|
157
165
|
}
|
|
158
166
|
method_calls[m_id] = method.calls
|
|
159
167
|
method_context[m_id] = {
|
|
168
|
+
"class_id": c_id,
|
|
160
169
|
"class_fqcn": cls.fqcn,
|
|
161
170
|
"local_types": method.local_types,
|
|
162
171
|
"field_types": cls.field_types,
|
|
163
172
|
"imports": parsed.imports,
|
|
164
173
|
"package": parsed.package,
|
|
165
174
|
}
|
|
166
|
-
class_methods[
|
|
175
|
+
class_methods[c_id][method.signature] = m_id
|
|
167
176
|
files_indexed += 1
|
|
168
177
|
self._emit(
|
|
169
178
|
progress,
|
|
@@ -182,7 +191,12 @@ class JavaIndexer:
|
|
|
182
191
|
self._emit(progress, "resolve_calls_done", calls_resolved=calls_resolved)
|
|
183
192
|
|
|
184
193
|
self._emit(progress, "resolve_types_start")
|
|
185
|
-
type_relationships += self._build_inheritance_edges(
|
|
194
|
+
type_relationships += self._build_inheritance_edges(
|
|
195
|
+
class_meta,
|
|
196
|
+
class_catalog,
|
|
197
|
+
class_methods,
|
|
198
|
+
fqcn_to_class_ids,
|
|
199
|
+
)
|
|
186
200
|
self._emit(progress, "resolve_types_done", type_relationships=type_relationships)
|
|
187
201
|
|
|
188
202
|
return IndexResult(
|
|
@@ -225,7 +239,7 @@ class JavaIndexer:
|
|
|
225
239
|
"""
|
|
226
240
|
MATCH (m:Method), (c:Class), (f:File)
|
|
227
241
|
WHERE m.class_id = c.id AND c.file_id = f.id AND f.project_id = $pid
|
|
228
|
-
RETURN m.id as method_id, m.name as name, m.signature as signature, c.fqcn as class_fqcn
|
|
242
|
+
RETURN m.id as method_id, m.name as name, m.signature as signature, c.fqcn as class_fqcn, c.id as class_id
|
|
229
243
|
""",
|
|
230
244
|
{"pid": project_id},
|
|
231
245
|
)
|
|
@@ -239,9 +253,30 @@ class JavaIndexer:
|
|
|
239
253
|
"name": r.get("name", ""),
|
|
240
254
|
"param_count": param_count,
|
|
241
255
|
"class_fqcn": r.get("class_fqcn", ""),
|
|
256
|
+
"class_id": r.get("class_id", ""),
|
|
242
257
|
}
|
|
243
258
|
return out
|
|
244
259
|
|
|
260
|
+
def _existing_class_ids_by_fqcn(self, project_id: str) -> dict[str, list[str]]:
|
|
261
|
+
recs = self.store.query_records(
|
|
262
|
+
"""
|
|
263
|
+
MATCH (c:Class), (f:File)
|
|
264
|
+
WHERE c.file_id = f.id AND f.project_id = $pid
|
|
265
|
+
RETURN c.fqcn as fqcn, c.id as class_id
|
|
266
|
+
""",
|
|
267
|
+
{"pid": project_id},
|
|
268
|
+
)
|
|
269
|
+
out: dict[str, list[str]] = {}
|
|
270
|
+
for r in recs:
|
|
271
|
+
fqcn = r.get("fqcn", "")
|
|
272
|
+
cid = r.get("class_id", "")
|
|
273
|
+
if not fqcn or not cid:
|
|
274
|
+
continue
|
|
275
|
+
out.setdefault(fqcn, [])
|
|
276
|
+
if cid not in out[fqcn]:
|
|
277
|
+
out[fqcn].append(cid)
|
|
278
|
+
return out
|
|
279
|
+
|
|
245
280
|
def _existing_class_catalog(self, project_id: str) -> dict[str, list[str]]:
|
|
246
281
|
recs = self.store.query_records(
|
|
247
282
|
"""
|
|
@@ -263,14 +298,17 @@ class JavaIndexer:
|
|
|
263
298
|
"""
|
|
264
299
|
MATCH (m:Method), (c:Class), (f:File)
|
|
265
300
|
WHERE m.class_id = c.id AND c.file_id = f.id AND f.project_id = $pid
|
|
266
|
-
RETURN c.
|
|
301
|
+
RETURN c.id as class_id, m.signature as signature, m.id as method_id
|
|
267
302
|
""",
|
|
268
303
|
{"pid": project_id},
|
|
269
304
|
)
|
|
270
305
|
out: dict[str, dict[str, str]] = {}
|
|
271
306
|
for r in recs:
|
|
272
|
-
|
|
273
|
-
|
|
307
|
+
class_key = r.get("class_id")
|
|
308
|
+
if not class_key:
|
|
309
|
+
continue
|
|
310
|
+
out.setdefault(class_key, {})
|
|
311
|
+
out[class_key][r["signature"]] = r["method_id"]
|
|
274
312
|
return out
|
|
275
313
|
|
|
276
314
|
@staticmethod
|
|
@@ -302,34 +340,34 @@ class JavaIndexer:
|
|
|
302
340
|
class_meta: dict[str, dict],
|
|
303
341
|
class_catalog: dict[str, list[str]],
|
|
304
342
|
class_methods: dict[str, dict[str, str]],
|
|
343
|
+
fqcn_to_class_ids: dict[str, list[str]],
|
|
305
344
|
) -> int:
|
|
306
345
|
rel_count = 0
|
|
307
|
-
for
|
|
308
|
-
src_id = class_id(fqcn)
|
|
346
|
+
for src_id, meta in class_meta.items():
|
|
309
347
|
ctx = {"package": meta.get("package", ""), "imports": meta.get("imports", [])}
|
|
310
348
|
|
|
311
349
|
parent_candidates = self._resolve_type_candidates(meta.get("extends"), ctx, class_catalog)
|
|
312
350
|
for parent_fqcn in parent_candidates:
|
|
313
|
-
dst_id
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
351
|
+
for dst_id in fqcn_to_class_ids.get(parent_fqcn, []):
|
|
352
|
+
self.store.add_reference("IMPLEMENTS", "Class", src_id, "Class", dst_id, 0.8)
|
|
353
|
+
rel_count += 1
|
|
354
|
+
for sig, method_id in class_methods.get(src_id, {}).items():
|
|
355
|
+
parent_method = class_methods.get(dst_id, {}).get(sig)
|
|
356
|
+
if parent_method:
|
|
357
|
+
self.store.add_reference("OVERRIDES", "Method", method_id, "Method", parent_method, 1.0)
|
|
358
|
+
rel_count += 1
|
|
321
359
|
|
|
322
360
|
for iface in meta.get("interfaces", []):
|
|
323
361
|
iface_candidates = self._resolve_type_candidates(iface, ctx, class_catalog)
|
|
324
362
|
for iface_fqcn in iface_candidates:
|
|
325
|
-
dst_id
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
363
|
+
for dst_id in fqcn_to_class_ids.get(iface_fqcn, []):
|
|
364
|
+
self.store.add_reference("IMPLEMENTS", "Class", src_id, "Class", dst_id, 1.0)
|
|
365
|
+
rel_count += 1
|
|
366
|
+
for sig, method_id in class_methods.get(src_id, {}).items():
|
|
367
|
+
iface_method = class_methods.get(dst_id, {}).get(sig)
|
|
368
|
+
if iface_method:
|
|
369
|
+
self.store.add_reference("OVERRIDES", "Method", method_id, "Method", iface_method, 1.0)
|
|
370
|
+
rel_count += 1
|
|
333
371
|
return rel_count
|
|
334
372
|
|
|
335
373
|
@staticmethod
|
|
@@ -337,3 +375,13 @@ class JavaIndexer:
|
|
|
337
375
|
if progress is None:
|
|
338
376
|
return
|
|
339
377
|
progress(event, payload)
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def _scope_from_rel_path(rel_path: str) -> str:
|
|
381
|
+
normalized = rel_path.replace("\\", "/")
|
|
382
|
+
if "/java/" in normalized:
|
|
383
|
+
return normalized.split("/java/", 1)[0]
|
|
384
|
+
if "/src/" in normalized:
|
|
385
|
+
return normalized.split("/src/", 1)[0]
|
|
386
|
+
scope = os.path.dirname(normalized).strip()
|
|
387
|
+
return scope or "."
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class SymbolRef:
|
|
9
|
+
symbol_id: str
|
|
10
|
+
method_id: str
|
|
11
|
+
class_id: str
|
|
12
|
+
file_id: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def digest_bytes(payload: bytes) -> str:
|
|
16
|
+
return hashlib.sha1(payload).hexdigest()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def file_id(project_id: str, rel_path: str) -> str:
|
|
20
|
+
return hashlib.sha1(f"{project_id}:{rel_path}".encode("utf-8")).hexdigest()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def class_id(fqcn: str, scope: str | None = None) -> str:
|
|
24
|
+
key = f"{scope}::{fqcn}" if scope else fqcn
|
|
25
|
+
return hashlib.sha1(key.encode("utf-8")).hexdigest()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def method_id(fqcn: str, signature: str, scope: str | None = None) -> str:
|
|
29
|
+
key = f"{scope}::{fqcn}#{signature}" if scope else f"{fqcn}#{signature}"
|
|
30
|
+
return hashlib.sha1(key.encode("utf-8")).hexdigest()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def symbol_id(kind: str, fqname: str, scope: str | None = None) -> str:
|
|
34
|
+
key = f"{kind}:{scope}:{fqname}" if scope else f"{kind}:{fqname}"
|
|
35
|
+
return hashlib.sha1(key.encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
from codespine.indexer.call_resolver import resolve_calls
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_resolver_prefers_receiver_type_and_arity():
|
|
7
|
+
method_catalog = {
|
|
8
|
+
"src": {
|
|
9
|
+
"name": "entry",
|
|
10
|
+
"param_count": 0,
|
|
11
|
+
"class_id": "c_service",
|
|
12
|
+
"class_fqcn": "com.example.Service",
|
|
13
|
+
"signature": "entry()",
|
|
14
|
+
},
|
|
15
|
+
"m1": {"name": "run", "param_count": 0, "class_id": "c_service", "class_fqcn": "com.example.Service", "signature": "run()"},
|
|
16
|
+
"m2": {
|
|
17
|
+
"name": "run",
|
|
18
|
+
"param_count": 1,
|
|
19
|
+
"class_id": "c_service",
|
|
20
|
+
"class_fqcn": "com.example.Service",
|
|
21
|
+
"signature": "run(String)",
|
|
22
|
+
},
|
|
23
|
+
"m3": {"name": "save", "param_count": 0, "class_id": "c_repo", "class_fqcn": "com.example.Repo", "signature": "save()"},
|
|
24
|
+
}
|
|
25
|
+
calls = {
|
|
26
|
+
"src": [
|
|
27
|
+
SimpleNamespace(name="run", receiver="this", arg_count=0),
|
|
28
|
+
SimpleNamespace(name="save", receiver="repo", arg_count=0),
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
method_context = {
|
|
32
|
+
"src": {
|
|
33
|
+
"class_id": "c_service",
|
|
34
|
+
"class_fqcn": "com.example.Service",
|
|
35
|
+
"local_types": {"repo": "Repo"},
|
|
36
|
+
"field_types": {},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
class_catalog = {"Service": ["com.example.Service"], "Repo": ["com.example.Repo"]}
|
|
40
|
+
|
|
41
|
+
out = list(resolve_calls(method_catalog, calls, method_context, class_catalog))
|
|
42
|
+
assert ("src", "m1", 1.0, "receiver_this_exact") in out
|
|
43
|
+
assert ("src", "m3", 0.8, "receiver_method_match") in out
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
pytest.importorskip("kuzu")
|
|
6
|
+
pytest.importorskip("tree_sitter_java")
|
|
7
|
+
|
|
8
|
+
from codespine.db.store import GraphStore
|
|
9
|
+
from codespine.indexer.engine import JavaIndexer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _write_java(path: Path, content: str) -> None:
|
|
13
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
path.write_text(content, encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_multimodule_duplicate_fqcn_is_indexed_without_collision(tmp_path: Path):
|
|
18
|
+
_write_java(
|
|
19
|
+
tmp_path / "module-a" / "src" / "main" / "java" / "com" / "example" / "App.java",
|
|
20
|
+
"""
|
|
21
|
+
package com.example;
|
|
22
|
+
public class App { public void fromA() {} }
|
|
23
|
+
""",
|
|
24
|
+
)
|
|
25
|
+
_write_java(
|
|
26
|
+
tmp_path / "module-b" / "src" / "main" / "java" / "com" / "example" / "App.java",
|
|
27
|
+
"""
|
|
28
|
+
package com.example;
|
|
29
|
+
public class App { public void fromB() {} }
|
|
30
|
+
""",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
store = GraphStore(read_only=False)
|
|
34
|
+
result = JavaIndexer(store).index_project(str(tmp_path), full=True)
|
|
35
|
+
|
|
36
|
+
classes = store.query_records(
|
|
37
|
+
"""
|
|
38
|
+
MATCH (c:Class), (f:File)
|
|
39
|
+
WHERE c.file_id = f.id AND f.project_id = $pid AND c.fqcn = $fqcn
|
|
40
|
+
RETURN c.id as id, f.path as path
|
|
41
|
+
""",
|
|
42
|
+
{"pid": result.project_id, "fqcn": "com.example.App"},
|
|
43
|
+
)
|
|
44
|
+
methods = store.query_records(
|
|
45
|
+
"""
|
|
46
|
+
MATCH (m:Method), (c:Class), (f:File)
|
|
47
|
+
WHERE m.class_id = c.id AND c.file_id = f.id AND f.project_id = $pid
|
|
48
|
+
RETURN m.name as name
|
|
49
|
+
""",
|
|
50
|
+
{"pid": result.project_id},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert len(classes) == 2
|
|
54
|
+
assert len({c["id"] for c in classes}) == 2
|
|
55
|
+
assert {"fromA", "fromB"}.issubset({m["name"] for m in methods})
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@dataclass
|
|
8
|
-
class SymbolRef:
|
|
9
|
-
symbol_id: str
|
|
10
|
-
method_id: str
|
|
11
|
-
class_id: str
|
|
12
|
-
file_id: str
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def digest_bytes(payload: bytes) -> str:
|
|
16
|
-
return hashlib.sha1(payload).hexdigest()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def file_id(project_id: str, rel_path: str) -> str:
|
|
20
|
-
return hashlib.sha1(f"{project_id}:{rel_path}".encode("utf-8")).hexdigest()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def class_id(fqcn: str) -> str:
|
|
24
|
-
return hashlib.sha1(fqcn.encode("utf-8")).hexdigest()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def method_id(fqcn: str, signature: str) -> str:
|
|
28
|
-
return hashlib.sha1(f"{fqcn}#{signature}".encode("utf-8")).hexdigest()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def symbol_id(kind: str, fqname: str) -> str:
|
|
32
|
-
return hashlib.sha1(f"{kind}:{fqname}".encode("utf-8")).hexdigest()
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
from types import SimpleNamespace
|
|
2
|
-
|
|
3
|
-
from codespine.indexer.call_resolver import resolve_calls
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_resolver_prefers_receiver_type_and_arity():
|
|
7
|
-
method_catalog = {
|
|
8
|
-
"src": {"name": "entry", "param_count": 0, "class_fqcn": "com.example.Service", "signature": "entry()"},
|
|
9
|
-
"m1": {"name": "run", "param_count": 0, "class_fqcn": "com.example.Service", "signature": "run()"},
|
|
10
|
-
"m2": {"name": "run", "param_count": 1, "class_fqcn": "com.example.Service", "signature": "run(String)"},
|
|
11
|
-
"m3": {"name": "save", "param_count": 0, "class_fqcn": "com.example.Repo", "signature": "save()"},
|
|
12
|
-
}
|
|
13
|
-
calls = {
|
|
14
|
-
"src": [
|
|
15
|
-
SimpleNamespace(name="run", receiver="this", arg_count=0),
|
|
16
|
-
SimpleNamespace(name="save", receiver="repo", arg_count=0),
|
|
17
|
-
]
|
|
18
|
-
}
|
|
19
|
-
method_context = {
|
|
20
|
-
"src": {
|
|
21
|
-
"class_fqcn": "com.example.Service",
|
|
22
|
-
"local_types": {"repo": "Repo"},
|
|
23
|
-
"field_types": {},
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
class_catalog = {"Service": ["com.example.Service"], "Repo": ["com.example.Repo"]}
|
|
27
|
-
|
|
28
|
-
out = resolve_calls(method_catalog, calls, method_context, class_catalog)
|
|
29
|
-
assert ("src", "m1", 1.0, "receiver_this_exact") in out
|
|
30
|
-
assert ("src", "m3", 0.8, "receiver_method_match") in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|