codecortex 0.7.0__tar.gz → 0.8.0__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.
Potentially problematic release.
This version of codecortex might be problematic. Click here for more details.
- {codecortex-0.7.0/src/codecortex.egg-info → codecortex-0.8.0}/PKG-INFO +2 -1
- {codecortex-0.7.0 → codecortex-0.8.0}/pyproject.toml +6 -1
- {codecortex-0.7.0 → codecortex-0.8.0/src/codecortex.egg-info}/PKG-INFO +2 -1
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codecortex.egg-info/SOURCES.txt +2 -1
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codecortex.egg-info/requires.txt +1 -0
- codecortex-0.8.0/src/codeintel/__init__.py +1 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/indexer.py +148 -8
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_chunking.py +13 -6
- codecortex-0.8.0/tests/test_treesitter.py +185 -0
- codecortex-0.7.0/src/codeintel/__init__.py +0 -1
- {codecortex-0.7.0 → codecortex-0.8.0}/LICENSE +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/README.md +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/setup.cfg +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/__main__.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/auth.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/cache.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/config.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/doctor.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/gateway.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/http_server.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/injector.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/installer.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/mapper.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/metrics.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/policy.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/provider.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/providers/graph.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/reset.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/searcher.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/semantic_db.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/server.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/src/codeintel/term.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_cache.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_config.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_doctor.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_e2e.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_enterprise.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_gateway.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_graph_provider.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_graph_real.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_hardening.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_http_auth.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_http_server.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_integration.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_lsp_real.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_mapper.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_never_raise.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_onboarding.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_rbac.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_reindexer.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_rerank.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_reset.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_semantic_provider.py +0 -0
- {codecortex-0.7.0 → codecortex-0.8.0}/tests/test_term.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
|
|
5
5
|
Author: Shammai Hamilton
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,7 @@ License-File: LICENSE
|
|
|
26
26
|
Requires-Dist: mcp>=1.0
|
|
27
27
|
Requires-Dist: sqlite-vec>=0.1
|
|
28
28
|
Requires-Dist: fastembed>=0.3
|
|
29
|
+
Requires-Dist: tree-sitter-language-pack>=1.0
|
|
29
30
|
Provides-Extra: dev
|
|
30
31
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
32
|
Requires-Dist: numpy>=1.24; extra == "dev"
|
|
@@ -31,7 +31,12 @@ classifiers = [
|
|
|
31
31
|
"Topic :: Software Development :: Quality Assurance",
|
|
32
32
|
"Typing :: Typed",
|
|
33
33
|
]
|
|
34
|
-
dependencies = [
|
|
34
|
+
dependencies = [
|
|
35
|
+
"mcp>=1.0",
|
|
36
|
+
"sqlite-vec>=0.1",
|
|
37
|
+
"fastembed>=0.3",
|
|
38
|
+
"tree-sitter-language-pack>=1.0", # def-aligned chunking for non-Python languages (P3)
|
|
39
|
+
]
|
|
35
40
|
|
|
36
41
|
[project.urls]
|
|
37
42
|
Homepage = "https://github.com/hamilton-sky/codeintel"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
|
|
5
5
|
Author: Shammai Hamilton
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,7 @@ License-File: LICENSE
|
|
|
26
26
|
Requires-Dist: mcp>=1.0
|
|
27
27
|
Requires-Dist: sqlite-vec>=0.1
|
|
28
28
|
Requires-Dist: fastembed>=0.3
|
|
29
|
+
Requires-Dist: tree-sitter-language-pack>=1.0
|
|
29
30
|
Provides-Extra: dev
|
|
30
31
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
32
|
Requires-Dist: numpy>=1.24; extra == "dev"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
|
@@ -14,7 +14,10 @@ if TYPE_CHECKING:
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
_INDEXED_EXTS = frozenset({
|
|
17
|
-
".py", ".
|
|
17
|
+
".py", ".md",
|
|
18
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", # TS/JS variants
|
|
19
|
+
".go", ".rs", ".java",
|
|
20
|
+
".c", ".h", ".cpp", ".cc", ".cxx", ".hpp", ".hh", # C/C++ variants
|
|
18
21
|
})
|
|
19
22
|
_SKIP_DIRS = frozenset({"__pycache__", ".git", "node_modules"})
|
|
20
23
|
# Vendored / regenerable dirs skipped even without a .gitignore entry.
|
|
@@ -41,6 +44,49 @@ def _pos_int(val: object, default: int) -> int:
|
|
|
41
44
|
return n if n > 0 else default
|
|
42
45
|
|
|
43
46
|
|
|
47
|
+
# ---- tree-sitter chunking (P3): def-aligned chunks for non-Python languages -----------------
|
|
48
|
+
# Extends the P1 chunker interface to TS/JS/Go/Rust/Java/C/C++ via tree-sitter, behind the same
|
|
49
|
+
# _primary_spans → _cover pipeline. The grammar pack is a normal dependency but never a hard
|
|
50
|
+
# requirement: if it (or a grammar) is unavailable, or a file's language config is missing, the
|
|
51
|
+
# file falls back to line windowing — exactly like a Python parse failure.
|
|
52
|
+
|
|
53
|
+
# file extension -> tree_sitter_language_pack language name. Only the *code* exts in
|
|
54
|
+
# _INDEXED_EXTS; .md (and anything unmapped) keeps line-windowing.
|
|
55
|
+
_TS_LANG_BY_EXT = {
|
|
56
|
+
".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "javascript",
|
|
57
|
+
".mjs": "javascript", ".cjs": "javascript",
|
|
58
|
+
".go": "go", ".rs": "rust", ".java": "java",
|
|
59
|
+
".c": "c", ".h": "cpp", ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
|
|
60
|
+
".hpp": "cpp", ".hh": "cpp",
|
|
61
|
+
}
|
|
62
|
+
# "Leaf" definition node types — each emitted as one whole chunk (like a top-level Python def).
|
|
63
|
+
_TS_FUNC_TYPES = {
|
|
64
|
+
"typescript": {"function_declaration", "generator_function_declaration", "method_definition",
|
|
65
|
+
"method_signature", "abstract_method_signature", "function_signature"},
|
|
66
|
+
"tsx": {"function_declaration", "generator_function_declaration", "method_definition",
|
|
67
|
+
"method_signature", "abstract_method_signature", "function_signature"},
|
|
68
|
+
"javascript": {"function_declaration", "generator_function_declaration", "method_definition"},
|
|
69
|
+
"go": {"function_declaration", "method_declaration", "type_declaration"},
|
|
70
|
+
"rust": {"function_item", "function_signature_item", "struct_item", "enum_item",
|
|
71
|
+
"union_item", "type_item", "macro_definition"},
|
|
72
|
+
"java": {"method_declaration", "constructor_declaration"},
|
|
73
|
+
"c": {"function_definition", "struct_specifier", "enum_specifier", "union_specifier"},
|
|
74
|
+
"cpp": {"function_definition"},
|
|
75
|
+
}
|
|
76
|
+
# "Container" definition types — emitted as a header chunk + one chunk per nested member, exactly
|
|
77
|
+
# like a Python class (so per-method chunks are never shadowed by a whole-class chunk).
|
|
78
|
+
_TS_CONTAINER_TYPES = {
|
|
79
|
+
"typescript": {"class_declaration", "abstract_class_declaration", "interface_declaration"},
|
|
80
|
+
"tsx": {"class_declaration", "abstract_class_declaration", "interface_declaration"},
|
|
81
|
+
"javascript": {"class_declaration"},
|
|
82
|
+
"go": set(),
|
|
83
|
+
"rust": {"impl_item", "trait_item", "mod_item"},
|
|
84
|
+
"java": {"class_declaration", "interface_declaration", "enum_declaration", "record_declaration"},
|
|
85
|
+
"c": set(),
|
|
86
|
+
"cpp": {"class_specifier", "struct_specifier", "namespace_definition"},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
44
90
|
class Indexer:
|
|
45
91
|
def __init__(
|
|
46
92
|
self,
|
|
@@ -76,6 +122,7 @@ class Indexer:
|
|
|
76
122
|
else 2 * self.window
|
|
77
123
|
)
|
|
78
124
|
self._embedder = None
|
|
125
|
+
self._ts_parsers: dict = {} # per-instance tree-sitter parser cache (lang -> parser|None)
|
|
79
126
|
|
|
80
127
|
def _get_embedder(self):
|
|
81
128
|
if self._embedder is None:
|
|
@@ -249,16 +296,109 @@ class Indexer:
|
|
|
249
296
|
n = len(lines)
|
|
250
297
|
return self._cover(self._primary_spans(tree, n), n)
|
|
251
298
|
|
|
299
|
+
def _get_ts_parser(self, lang: str):
|
|
300
|
+
"""Lazily load + cache (per instance) the tree-sitter parser for a language. Returns None
|
|
301
|
+
when ``tree-sitter-language-pack`` isn't installed or the grammar is unavailable — the
|
|
302
|
+
caller then windows (tree-sitter is a normal dep, never a hard requirement)."""
|
|
303
|
+
cache = self._ts_parsers
|
|
304
|
+
if lang in cache:
|
|
305
|
+
return cache[lang]
|
|
306
|
+
parser = None
|
|
307
|
+
try:
|
|
308
|
+
from tree_sitter_language_pack import get_parser
|
|
309
|
+
parser = get_parser(lang)
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
logger.debug("tree-sitter parser for %s unavailable: %s", lang, exc)
|
|
312
|
+
cache[lang] = parser
|
|
313
|
+
return parser
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def _ts_end_line(node, n: int) -> int:
|
|
317
|
+
"""0-based half-open end line of a tree-sitter node. ``end_point`` is (row, col); a col of 0
|
|
318
|
+
means the node ends at the start of that row (so the row isn't included). Clamped to
|
|
319
|
+
``[start+1, n]``."""
|
|
320
|
+
row, col = node.end_point
|
|
321
|
+
end = row if col == 0 else row + 1
|
|
322
|
+
return min(n, max(node.start_point[0] + 1, end))
|
|
323
|
+
|
|
324
|
+
def _primary_spans_ts(self, root, lang: str, n: int) -> list[tuple[int, int]]:
|
|
325
|
+
"""Def-aligned 'primary' spans from a tree-sitter tree, mirroring ``_primary_spans``: each
|
|
326
|
+
function/method → one span; each container (class/impl/trait/…) → a header span plus one
|
|
327
|
+
span per nested member. Node types the language config doesn't list are simply not treated
|
|
328
|
+
as defs — they fall into ``_cover``'s window-filled gaps, so an incomplete config degrades
|
|
329
|
+
precision, never correctness."""
|
|
330
|
+
func_types = _TS_FUNC_TYPES.get(lang, set())
|
|
331
|
+
cont_types = _TS_CONTAINER_TYPES.get(lang, set())
|
|
332
|
+
all_types = func_types | cont_types
|
|
333
|
+
if not all_types:
|
|
334
|
+
return [] # unknown language -> caller windows the whole file
|
|
335
|
+
spans: list[tuple[int, int]] = []
|
|
336
|
+
|
|
337
|
+
def nearest_defs(node):
|
|
338
|
+
"""Def-type nodes reachable without crossing another def-type — top-level defs from the
|
|
339
|
+
root, a container's direct members from the container. Iterative, so deep nesting can't
|
|
340
|
+
blow the stack."""
|
|
341
|
+
found = []
|
|
342
|
+
stack = [c for c in reversed(node.children) if c.is_named]
|
|
343
|
+
while stack:
|
|
344
|
+
c = stack.pop()
|
|
345
|
+
if c.type in all_types:
|
|
346
|
+
found.append(c)
|
|
347
|
+
else:
|
|
348
|
+
stack.extend(g for g in reversed(c.children) if g.is_named)
|
|
349
|
+
return found
|
|
350
|
+
|
|
351
|
+
def emit(node):
|
|
352
|
+
s = node.start_point[0]
|
|
353
|
+
e = self._ts_end_line(node, n)
|
|
354
|
+
if node.type in cont_types:
|
|
355
|
+
members = sorted(nearest_defs(node), key=lambda m: m.start_point[0])
|
|
356
|
+
if members:
|
|
357
|
+
header_end = max(s + 1, min(members[0].start_point[0], e))
|
|
358
|
+
spans.append((s, header_end)) # header: class/impl line -> first member
|
|
359
|
+
for m in members:
|
|
360
|
+
emit(m)
|
|
361
|
+
return
|
|
362
|
+
spans.append((s, e))
|
|
363
|
+
|
|
364
|
+
for node in sorted(nearest_defs(root), key=lambda x: x.start_point[0]):
|
|
365
|
+
emit(node)
|
|
366
|
+
return spans
|
|
367
|
+
|
|
368
|
+
def _chunk_treesitter(self, lines: list[str], source: str, lang: str):
|
|
369
|
+
"""Parse ``source`` with tree-sitter → a complete, def-aligned cover, or ``None`` when the
|
|
370
|
+
parser is unavailable (caller windows). tree-sitter is error-tolerant, so a syntactically
|
|
371
|
+
broken file still yields a partial tree (and thus useful spans) rather than raising."""
|
|
372
|
+
parser = self._get_ts_parser(lang)
|
|
373
|
+
if parser is None:
|
|
374
|
+
return None
|
|
375
|
+
tree = parser.parse(source.encode("utf-8", errors="replace"))
|
|
376
|
+
n = len(lines)
|
|
377
|
+
return self._cover(self._primary_spans_ts(tree.root_node, lang, n), n)
|
|
378
|
+
|
|
252
379
|
def _spans_for_file(
|
|
253
380
|
self, filepath: Path, lines: list[str], rel_path: str
|
|
254
381
|
) -> list[tuple[int, int]]:
|
|
255
|
-
"""Choose spans for one file
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
382
|
+
"""Choose spans for one file under the syntax strategy: ``ast`` for ``.py``, tree-sitter for
|
|
383
|
+
the mapped languages (TS/JS/Go/Rust/Java/C/C++), fixed windows for everything else and on
|
|
384
|
+
any parse/grammar failure or when tree-sitter isn't installed."""
|
|
385
|
+
if self.chunk_strategy == "syntax":
|
|
386
|
+
suffix = filepath.suffix.lower()
|
|
387
|
+
if suffix == ".py":
|
|
388
|
+
try:
|
|
389
|
+
return self._chunk_python_ast(lines, "".join(lines))
|
|
390
|
+
except Exception as exc:
|
|
391
|
+
logger.debug("syntax chunking failed for %s (%s) — windowing", rel_path, exc)
|
|
392
|
+
else:
|
|
393
|
+
lang = _TS_LANG_BY_EXT.get(suffix)
|
|
394
|
+
if lang is not None:
|
|
395
|
+
try:
|
|
396
|
+
spans = self._chunk_treesitter(lines, "".join(lines), lang)
|
|
397
|
+
if spans is not None:
|
|
398
|
+
return spans
|
|
399
|
+
except Exception as exc:
|
|
400
|
+
logger.debug("tree-sitter chunking failed for %s (%s) — windowing",
|
|
401
|
+
rel_path, exc)
|
|
262
402
|
return self._window_spans(0, len(lines))
|
|
263
403
|
|
|
264
404
|
# ---- materialisation -------------------------------------------------------------------
|
|
@@ -195,12 +195,19 @@ def test_nul_byte_source_falls_back_without_raising():
|
|
|
195
195
|
assert spans == idx._window_spans(0, len(lines))
|
|
196
196
|
|
|
197
197
|
|
|
198
|
-
def
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
def test_non_code_exts_window_but_treesitter_langs_do_not():
|
|
199
|
+
# .md / unmapped exts are never parsed → line windows (as of 0.8.0 tree-sitter handles the
|
|
200
|
+
# code langs; see tests/test_treesitter.py for the def-aligned behavior)
|
|
201
|
+
csv = "".join(f"col{i},val{i}\n" for i in range(30))
|
|
202
|
+
csv_lines = csv.splitlines(keepends=True)
|
|
203
|
+
idx = Indexer(_mem_db())
|
|
204
|
+
for name in ("data.md", "notes.txt"):
|
|
205
|
+
assert idx._spans_for_file(Path(name), csv_lines, name) == idx._window_spans(0, len(csv_lines))
|
|
206
|
+
# a real .ts file with definitions is def-aligned, NOT windowed
|
|
207
|
+
ts = "function a() {\n return 1;\n}\nfunction b() {\n return 2;\n}\n".splitlines(keepends=True)
|
|
208
|
+
ts_spans = idx._spans_for_file(Path("app.ts"), ts, "app.ts")
|
|
209
|
+
assert ts_spans != idx._window_spans(0, len(ts))
|
|
210
|
+
assert (0, 3) in ts_spans and (3, 6) in ts_spans # each function its own chunk
|
|
204
211
|
|
|
205
212
|
|
|
206
213
|
# --------------------------------------------------------------------------- 5: "lines" parity
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Tree-sitter chunking (0.8.0, Phase 3 of docs/roadmap-semantic.md).
|
|
2
|
+
|
|
3
|
+
Extends 0.6.0 syntax-aware chunking to non-Python languages (TS/JS/Go/Rust/Java/C/C++) via
|
|
4
|
+
tree-sitter, behind the same `_primary_spans → _cover` pipeline. `tree-sitter-language-pack` is a
|
|
5
|
+
normal dependency; these tests exercise real grammars, plus the graceful fallback to line-windowing
|
|
6
|
+
when a parser is unavailable or a file fails to yield defs.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from codeintel.indexer import Indexer, _INDEXED_EXTS, _TS_LANG_BY_EXT
|
|
16
|
+
from codeintel.semantic_db import SemanticDb
|
|
17
|
+
|
|
18
|
+
# A normal dependency — but skip cleanly rather than error if a platform lacks a wheel.
|
|
19
|
+
pytest.importorskip("tree_sitter_language_pack")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _idx(**kw) -> Indexer:
|
|
23
|
+
db = SemanticDb(":memory:")
|
|
24
|
+
db.init()
|
|
25
|
+
return Indexer(db, **kw)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _spans(name: str, src: str, **kw):
|
|
29
|
+
lines = src.splitlines(keepends=True)
|
|
30
|
+
return _idx(**kw)._spans_for_file(Path(name), lines, name), len(lines)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _assert_gapless_cover(spans, n):
|
|
34
|
+
assert spans and spans[0][0] == 0, f"must start at 0: {spans}"
|
|
35
|
+
assert len({s for s, _ in spans}) == len(spans), "duplicate start → chunk_id collision"
|
|
36
|
+
frontier = 0
|
|
37
|
+
for s, e in sorted(spans):
|
|
38
|
+
assert s < e, f"empty/negative span {(s, e)}"
|
|
39
|
+
assert s <= frontier, f"gap before {(s, e)} (covered to {frontier})"
|
|
40
|
+
frontier = max(frontier, e)
|
|
41
|
+
assert frontier == n, f"must cover to {n}, reached {frontier}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --------------------------------------------------------------------------- per-language def-alignment
|
|
45
|
+
|
|
46
|
+
def test_typescript_class_and_function_are_def_aligned():
|
|
47
|
+
src = (
|
|
48
|
+
"export class Widget {\n" # 0 class header
|
|
49
|
+
" greet(name: string) {\n" # 1 method
|
|
50
|
+
" return name;\n" # 2
|
|
51
|
+
" }\n" # 3
|
|
52
|
+
"}\n" # 4
|
|
53
|
+
"function top() {\n" # 5 top-level function
|
|
54
|
+
" return 1;\n" # 6
|
|
55
|
+
"}\n" # 7
|
|
56
|
+
)
|
|
57
|
+
spans, n = _spans("a.ts", src)
|
|
58
|
+
_assert_gapless_cover(spans, n)
|
|
59
|
+
starts = {s for s, _ in spans}
|
|
60
|
+
assert 0 in starts # class header chunk begins on the class line
|
|
61
|
+
assert 1 in starts # the method is its own chunk (not shadowed by a whole-class chunk)
|
|
62
|
+
assert 5 in starts # the top-level function is its own chunk
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_go_functions_and_methods():
|
|
66
|
+
src = ("package p\n\n"
|
|
67
|
+
"func Free() int {\n\treturn 1\n}\n\n"
|
|
68
|
+
"func (t T) Method() int {\n\treturn 2\n}\n")
|
|
69
|
+
spans, n = _spans("b.go", src)
|
|
70
|
+
_assert_gapless_cover(spans, n)
|
|
71
|
+
starts = {s for s, _ in spans}
|
|
72
|
+
assert 2 in starts # func Free at line 2 (0-based)
|
|
73
|
+
assert 6 in starts # method at line 6
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_rust_impl_method_is_its_own_chunk():
|
|
77
|
+
src = ("struct S { x: i32 }\n\n"
|
|
78
|
+
"impl S {\n"
|
|
79
|
+
" fn method(&self) -> i32 {\n"
|
|
80
|
+
" self.x\n"
|
|
81
|
+
" }\n"
|
|
82
|
+
"}\n")
|
|
83
|
+
spans, n = _spans("c.rs", src)
|
|
84
|
+
_assert_gapless_cover(spans, n)
|
|
85
|
+
assert 3 in {s for s, _ in spans} # fn method at line 3, carved out of the impl block
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_java_class_method():
|
|
89
|
+
src = ("class A {\n"
|
|
90
|
+
" void m() {\n"
|
|
91
|
+
" return;\n"
|
|
92
|
+
" }\n"
|
|
93
|
+
"}\n")
|
|
94
|
+
spans, n = _spans("d.java", src)
|
|
95
|
+
_assert_gapless_cover(spans, n)
|
|
96
|
+
starts = {s for s, _ in spans}
|
|
97
|
+
assert 0 in starts and 1 in starts # class header + method m
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_cpp_class_and_free_function():
|
|
101
|
+
src = ("class C {\npublic:\n int m() { return 1; }\n};\nint free_fn() { return 2; }\n")
|
|
102
|
+
spans, n = _spans("e.cpp", src)
|
|
103
|
+
_assert_gapless_cover(spans, n)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_multiple_functions_are_separate_chunks():
|
|
107
|
+
src = "function a() {\n return 1;\n}\nfunction b() {\n return 2;\n}\n"
|
|
108
|
+
spans, n = _spans("m.ts", src)
|
|
109
|
+
_assert_gapless_cover(spans, n)
|
|
110
|
+
assert (0, 3) in spans and (3, 6) in spans # each function whole and distinct
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --------------------------------------------------------------------------- fallbacks / never-raise
|
|
114
|
+
|
|
115
|
+
def test_error_tolerant_source_still_covers_and_never_raises():
|
|
116
|
+
spans, n = _spans("bad.js", "this is @#$ not valid javascript !!!\n")
|
|
117
|
+
_assert_gapless_cover(spans, n) # tree-sitter is error-tolerant → partial tree, no raise
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_lines_strategy_bypasses_treesitter():
|
|
121
|
+
src = "function foo() {\n return 1;\n}\n"
|
|
122
|
+
lines = src.splitlines(keepends=True)
|
|
123
|
+
idx = _idx(chunk_strategy="lines")
|
|
124
|
+
assert idx._spans_for_file(Path("x.ts"), lines, "x.ts") == idx._window_spans(0, len(lines))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_unmapped_exts_window():
|
|
128
|
+
src = "".join(f"line {i}\n" for i in range(25))
|
|
129
|
+
lines = src.splitlines(keepends=True)
|
|
130
|
+
idx = _idx()
|
|
131
|
+
for name in ("readme.md", "notes.txt"):
|
|
132
|
+
assert idx._spans_for_file(Path(name), lines, name) == idx._window_spans(0, len(lines))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_falls_back_to_windowing_when_parser_unavailable():
|
|
136
|
+
src = "function foo() {\n return 1;\n}\n"
|
|
137
|
+
lines = src.splitlines(keepends=True)
|
|
138
|
+
idx = _idx()
|
|
139
|
+
with patch.object(idx, "_get_ts_parser", return_value=None): # simulate the dep not installed
|
|
140
|
+
assert idx._spans_for_file(Path("x.ts"), lines, "x.ts") == idx._window_spans(0, len(lines))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_never_raises_on_parser_exception():
|
|
144
|
+
src = "function foo() {\n return 1;\n}\n"
|
|
145
|
+
lines = src.splitlines(keepends=True)
|
|
146
|
+
idx = _idx()
|
|
147
|
+
|
|
148
|
+
class _Boom:
|
|
149
|
+
def parse(self, *a, **k):
|
|
150
|
+
raise RuntimeError("grammar exploded")
|
|
151
|
+
|
|
152
|
+
with patch.object(idx, "_get_ts_parser", return_value=_Boom()):
|
|
153
|
+
assert idx._spans_for_file(Path("x.ts"), lines, "x.ts") == idx._window_spans(0, len(lines))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_parser_cache_loads_each_language_once():
|
|
157
|
+
idx = _idx()
|
|
158
|
+
with patch("tree_sitter_language_pack.get_parser") as gp:
|
|
159
|
+
gp.return_value = object()
|
|
160
|
+
idx._get_ts_parser("go")
|
|
161
|
+
idx._get_ts_parser("go")
|
|
162
|
+
idx._get_ts_parser("rust")
|
|
163
|
+
assert gp.call_count == 2 # one load per distinct language, then cached
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_typescript_interface_decomposes_into_members():
|
|
167
|
+
# interface method members are `method_signature` (distinct from abstract-class members); the
|
|
168
|
+
# interface must decompose into a header + per-method chunks, not one whole-interface chunk
|
|
169
|
+
src = ("interface Repo {\n" # 0 header
|
|
170
|
+
" get(id: string): Item;\n" # 1 method_signature
|
|
171
|
+
" put(item: Item): void;\n" # 2 method_signature
|
|
172
|
+
"}\n") # 3
|
|
173
|
+
spans, n = _spans("r.ts", src)
|
|
174
|
+
_assert_gapless_cover(spans, n)
|
|
175
|
+
starts = {s for s, _ in spans}
|
|
176
|
+
assert 1 in starts and 2 in starts, f"interface methods must be their own chunks: {spans}"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_lang_map_and_indexed_exts_agree_both_ways():
|
|
180
|
+
code_exts = _INDEXED_EXTS - {".py", ".md"}
|
|
181
|
+
# every walked code ext maps to a tree-sitter language...
|
|
182
|
+
assert code_exts <= set(_TS_LANG_BY_EXT), f"walked but unmapped: {code_exts - set(_TS_LANG_BY_EXT)}"
|
|
183
|
+
# ...AND every mapped ext is actually walked (else it's indexed by nothing — silent omission)
|
|
184
|
+
assert set(_TS_LANG_BY_EXT) <= _INDEXED_EXTS, \
|
|
185
|
+
f"mapped but never walked: {set(_TS_LANG_BY_EXT) - _INDEXED_EXTS}"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.7.0"
|
|
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
|
|
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
|