code-symbol-index 0.3.2__tar.gz → 0.3.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-symbol-index
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Tree-sitter backed symbol search and inspection for codebases.
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-symbol-index
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Tree-sitter backed symbol search and inspection for codebases.
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: MIT
@@ -22,7 +22,7 @@ from tree_sitter import Node
22
22
  from tree_sitter_language_pack import get_parser
23
23
 
24
24
 
25
- __version__ = "0.3.2"
25
+ __version__ = "0.3.4"
26
26
  SCHEMA_VERSION = 5
27
27
  DEFAULT_INDEX_DIR = ".code-symbol-index"
28
28
  DEFAULT_INDEX_DB = "index.sqlite"
@@ -2516,7 +2516,7 @@ def _parse_file(
2516
2516
  except (OSError, UnicodeDecodeError, BinaryFileError):
2517
2517
  return None
2518
2518
 
2519
- tree = _parser_for_language(spec.name).parse(source_text)
2519
+ tree = _parse_source(_parser_for_language(spec.name), source_text)
2520
2520
  root_node = tree.root_node() if callable(tree.root_node) else tree.root_node
2521
2521
  source_bytes = source_text.encode("utf-8")
2522
2522
  symbols, references = _extract_symbols_and_references(
@@ -2579,6 +2579,16 @@ def _file_contains_bytes(path: Path, needle: bytes) -> bool:
2579
2579
  _PARSER_TLS = threading.local()
2580
2580
 
2581
2581
 
2582
+ def _parse_source(parser, source: str):
2583
+ # tree-sitter's parse() signature varies across versions/builds: some accept str, others
2584
+ # require bytes (raising "source must be a bytestring or a callable, not str"). Try str first,
2585
+ # then fall back to encoded bytes so both bindings work. Byte offsets are identical either way.
2586
+ try:
2587
+ return parser.parse(source)
2588
+ except TypeError:
2589
+ return parser.parse(source.encode("utf-8"))
2590
+
2591
+
2582
2592
  def _parser_for_language(language: str):
2583
2593
  if language not in LANGUAGE_BY_NAME:
2584
2594
  raise UnsupportedLanguageError(f"Unsupported language: {language}")
@@ -2751,8 +2761,8 @@ def _extract_symbols_and_references(
2751
2761
  name=_node_text(source, node),
2752
2762
  language=language.name,
2753
2763
  path=path,
2754
- range=_node_range(node),
2755
- context=_line_context(lines, node),
2764
+ range=_node_range(source, node),
2765
+ context=_line_context(source, lines, node),
2756
2766
  reference_kind=_classify_reference(node, parent, grandparent, ctx, language),
2757
2767
  )
2758
2768
  )
@@ -2782,7 +2792,7 @@ def _python_top_level_symbols(source: bytes, path: Path, language: LanguageSpec,
2782
2792
  if not name or not _looks_like_symbol_name(name):
2783
2793
  continue
2784
2794
  kind = "constant" if name.isupper() else "variable"
2785
- range_ = _node_range(name_node)
2795
+ range_ = _node_range(source, name_node)
2786
2796
  symbols.append(
2787
2797
  Symbol(
2788
2798
  id=_symbol_id(language.name, path, kind, name, range_.start_byte),
@@ -2821,7 +2831,7 @@ def _python_dict_key_symbols(
2821
2831
  key_name, key_node = _python_dict_key_name(source, language, key)
2822
2832
  if not key_name or not _looks_like_symbol_name(key_name):
2823
2833
  continue
2824
- range_ = _node_range(key_node)
2834
+ range_ = _node_range(source, key_node)
2825
2835
  symbols.append(
2826
2836
  Symbol(
2827
2837
  id=_symbol_id(language.name, path, "dict_key", key_name, range_.start_byte),
@@ -2864,7 +2874,7 @@ def _symbol_from_node(
2864
2874
  name = _node_text(source, name_node)
2865
2875
  if not name or not _looks_like_symbol_name(name):
2866
2876
  return None
2867
- range_ = _node_range(name_node)
2877
+ range_ = _node_range(source, name_node)
2868
2878
  return Symbol(
2869
2879
  id=_symbol_id(language.name, path, kind, name, range_.start_byte),
2870
2880
  name=name,
@@ -2913,25 +2923,28 @@ def _signature(source: bytes, node: Node) -> str:
2913
2923
  return first_line[:240]
2914
2924
 
2915
2925
 
2916
- def _node_range(node: Node) -> Range:
2917
- start_line, start_column = _point(_node_start_point(node))
2918
- end_line, end_column = _point(_node_end_point(node))
2926
+ def _node_range(source: bytes, node: Node) -> Range:
2927
+ start_byte = _node_start_byte(node)
2928
+ end_byte = _node_end_byte(node)
2929
+ start_line, start_column = _byte_position(source, start_byte)
2930
+ end_line, end_column = _byte_position(source, end_byte)
2919
2931
  return Range(
2920
2932
  start=Position(line=start_line, column=start_column),
2921
2933
  end=Position(line=end_line, column=end_column),
2922
- start_byte=_node_start_byte(node),
2923
- end_byte=_node_end_byte(node),
2934
+ start_byte=start_byte,
2935
+ end_byte=end_byte,
2924
2936
  )
2925
2937
 
2926
2938
 
2927
- def _point(point: Any) -> tuple[int, int]:
2928
- if hasattr(point, "row"):
2929
- return point.row, point.column
2930
- return point[0], point[1]
2939
+ def _byte_position(source: bytes, offset: int) -> tuple[int, int]:
2940
+ prefix = source[:offset]
2941
+ line = prefix.count(b"\n")
2942
+ line_start = prefix.rfind(b"\n") + 1
2943
+ return line, len(prefix) - line_start
2931
2944
 
2932
2945
 
2933
- def _line_context(lines: list[str], node: Node) -> str:
2934
- line, _ = _point(_node_start_point(node))
2946
+ def _line_context(source: bytes, lines: list[str], node: Node) -> str:
2947
+ line, _ = _byte_position(source, _node_start_byte(node))
2935
2948
  if 0 <= line < len(lines):
2936
2949
  return lines[line].strip()
2937
2950
  return ""
@@ -3685,13 +3698,14 @@ def _definition_ranges_for_symbols(
3685
3698
  spec = _spec_for_path(path, repo.languages)
3686
3699
  if spec is None:
3687
3700
  return {}
3701
+ source_bytes = source.encode("utf-8")
3688
3702
 
3689
3703
  wanted: dict[tuple[str, int], Symbol] = {
3690
3704
  (symbol.kind, symbol.range.start_byte): symbol
3691
3705
  for symbol in symbols
3692
3706
  }
3693
3707
  ranges: dict[str, Range] = {}
3694
- tree = _parser_for_language(spec.name).parse(source)
3708
+ tree = _parse_source(_parser_for_language(spec.name), source)
3695
3709
  root_node = tree.root_node() if callable(tree.root_node) else tree.root_node
3696
3710
 
3697
3711
  def walk(node: Node) -> None:
@@ -3703,7 +3717,7 @@ def _definition_ranges_for_symbols(
3703
3717
  if name_node is not None:
3704
3718
  symbol = wanted.get((kind, _node_start_byte(name_node)))
3705
3719
  if symbol is not None:
3706
- ranges[symbol.id] = _node_range(node)
3720
+ ranges[symbol.id] = _node_range(source_bytes, node)
3707
3721
  for child in _node_children(node):
3708
3722
  walk(child)
3709
3723
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-symbol-index"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "Tree-sitter backed symbol search and inspection for codebases."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -41,6 +41,42 @@ value = helper()
41
41
  assert "def helper" in (inspection.source_preview or "")
42
42
 
43
43
 
44
+ def test_python_ranges_do_not_depend_on_tree_sitter_points(monkeypatch) -> None:
45
+ source = (
46
+ "def caller():\n"
47
+ " return target()\n"
48
+ "\n"
49
+ + ("# padding\n" * 32)
50
+ + " def target():\n"
51
+ " return 1\n"
52
+ )
53
+ source_bytes = source.encode("utf-8")
54
+
55
+ def fail_point_access(*_args):
56
+ raise AssertionError("tree-sitter point access should not be used")
57
+
58
+ monkeypatch.setattr(code_symbol_index, "_node_start_point", fail_point_access)
59
+ monkeypatch.setattr(code_symbol_index, "_node_end_point", fail_point_access)
60
+
61
+ parser = code_symbol_index._parser_for_language("python")
62
+ tree = parser.parse(source_bytes)
63
+ root_node = tree.root_node() if callable(tree.root_node) else tree.root_node
64
+ symbols, references = code_symbol_index._extract_symbols_and_references(
65
+ source=source_bytes,
66
+ root_node=root_node,
67
+ path=Path("app.py"),
68
+ language=code_symbol_index.LANGUAGE_BY_NAME["python"],
69
+ include_references=True,
70
+ )
71
+
72
+ target = next(symbol for symbol in symbols if symbol.name == "target")
73
+ target_line = source.splitlines().index(" def target():")
74
+ assert target.range.start.line == target_line
75
+ assert target.range.start.column == 8
76
+ assert target.range.start_byte == source_bytes.rindex(b"target")
77
+ assert any(reference.name == "target" and reference.context == "return target()" for reference in references)
78
+
79
+
44
80
  def test_indexes_python_top_level_constants_and_dict_keys(tmp_path: Path, capsys) -> None:
45
81
  (tmp_path / "settings.py").write_text(
46
82
  "MODEL_NAME = 'ask-syft'\n"