scip-cli 1.0.3__tar.gz → 1.1.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.
Files changed (27) hide show
  1. {scip_cli-1.0.3/scip_cli.egg-info → scip_cli-1.1.0}/PKG-INFO +31 -1
  2. {scip_cli-1.0.3 → scip_cli-1.1.0}/README.md +30 -0
  3. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/SKILL.md +18 -8
  4. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/__init__.py +1 -1
  5. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/__main__.py +17 -0
  6. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/def_cmd.py +10 -2
  7. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/members.py +2 -1
  8. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/rdeps.py +9 -1
  9. scip_cli-1.1.0/scip_cli/commands/refs.py +116 -0
  10. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/search.py +10 -3
  11. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/symbols.py +10 -5
  12. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/lib.py +61 -18
  13. {scip_cli-1.0.3 → scip_cli-1.1.0/scip_cli.egg-info}/PKG-INFO +31 -1
  14. {scip_cli-1.0.3 → scip_cli-1.1.0}/tests/test_pure_functions.py +140 -1
  15. scip_cli-1.0.3/scip_cli/commands/refs.py +0 -83
  16. {scip_cli-1.0.3 → scip_cli-1.1.0}/LICENSE +0 -0
  17. {scip_cli-1.0.3 → scip_cli-1.1.0}/MANIFEST.in +0 -0
  18. {scip_cli-1.0.3 → scip_cli-1.1.0}/pyproject.toml +0 -0
  19. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/__init__.py +0 -0
  20. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/reindex.py +0 -0
  21. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/skill.py +0 -0
  22. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli.egg-info/SOURCES.txt +0 -0
  23. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli.egg-info/dependency_links.txt +0 -0
  24. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli.egg-info/entry_points.txt +0 -0
  25. {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli.egg-info/top_level.txt +0 -0
  26. {scip_cli-1.0.3 → scip_cli-1.1.0}/setup.cfg +0 -0
  27. {scip_cli-1.0.3 → scip_cli-1.1.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scip-cli
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: Fast code intelligence via SCIP indexes
5
5
  Home-page: https://github.com/flesler/scip-cli
6
6
  Author: Ariel Flesler
@@ -40,10 +40,27 @@ Fast code intelligence CLI for TypeScript/JavaScript and Python projects. Query
40
40
 
41
41
  ### 1. Install scip-cli
42
42
 
43
+ **From PyPI:**
44
+
43
45
  ```bash
44
46
  pip install scip-cli
45
47
  ```
46
48
 
49
+ **From source (local development):**
50
+
51
+ ```bash
52
+ git clone https://github.com/flesler/scip-cli.git
53
+ cd scip-cli
54
+ pip install .
55
+ ```
56
+
57
+ For editable development (where `pip install -e .` fails due to permissions):
58
+
59
+ ```bash
60
+ export PYTHONPATH=/path/to/scip-cli:$PYTHONPATH
61
+ python -m scip_cli --help
62
+ ```
63
+
47
64
  ### 2. Install prerequisites (optional)
48
65
 
49
66
  scip-cli can automatically download the required indexing tools when needed, or you can install them globally for faster performance:
@@ -156,6 +173,19 @@ scip_cli/
156
173
  └── skill.py
157
174
  ```
158
175
 
176
+ ## Development
177
+
178
+ ### Debug Logging
179
+
180
+ Set `SCIP_CLI_DEBUG=1` to enable SQL query logging to stderr:
181
+
182
+ ```bash
183
+ SCIP_CLI_DEBUG=1 scip-cli refs MyFunction
184
+ # Shows: SQL: SELECT ... | params: (...)
185
+ ```
186
+
187
+ This is useful for testing and debugging SQL queries without exposing a `--debug` flag to users.
188
+
159
189
  ## License
160
190
 
161
191
  MIT
@@ -16,10 +16,27 @@ Fast code intelligence CLI for TypeScript/JavaScript and Python projects. Query
16
16
 
17
17
  ### 1. Install scip-cli
18
18
 
19
+ **From PyPI:**
20
+
19
21
  ```bash
20
22
  pip install scip-cli
21
23
  ```
22
24
 
25
+ **From source (local development):**
26
+
27
+ ```bash
28
+ git clone https://github.com/flesler/scip-cli.git
29
+ cd scip-cli
30
+ pip install .
31
+ ```
32
+
33
+ For editable development (where `pip install -e .` fails due to permissions):
34
+
35
+ ```bash
36
+ export PYTHONPATH=/path/to/scip-cli:$PYTHONPATH
37
+ python -m scip_cli --help
38
+ ```
39
+
23
40
  ### 2. Install prerequisites (optional)
24
41
 
25
42
  scip-cli can automatically download the required indexing tools when needed, or you can install them globally for faster performance:
@@ -132,6 +149,19 @@ scip_cli/
132
149
  └── skill.py
133
150
  ```
134
151
 
152
+ ## Development
153
+
154
+ ### Debug Logging
155
+
156
+ Set `SCIP_CLI_DEBUG=1` to enable SQL query logging to stderr:
157
+
158
+ ```bash
159
+ SCIP_CLI_DEBUG=1 scip-cli refs MyFunction
160
+ # Shows: SQL: SELECT ... | params: (...)
161
+ ```
162
+
163
+ This is useful for testing and debugging SQL queries without exposing a `--debug` flag to users.
164
+
135
165
  ## License
136
166
 
137
167
  MIT
@@ -11,8 +11,8 @@ All commands are sub-commands of `scip-cli`. Run from the project root.
11
11
 
12
12
  | Question | Use | What you get |
13
13
  |----------|-----|--------------|
14
- | "Where is X defined and what does it do?" | `def X` | Functions: full body. Classes: full definition. Multiple exact matches returned together |
15
- | "Where is X used/called?" | `refs X` | All file:line locations (~0.03s). Ambiguous bare names first match only |
14
+ | "Where is X defined and what does it do?" | `def X` | Functions: full body. Classes: full definition. Use `--limit` to cap results |
15
+ | "Where is X used/called?" | `refs X` | All file:line locations. Shows refs for all matching symbols. Use `--limit` to cap |
16
16
  | "What's in this file?" | `symbols file` | All symbols — bare filename works (`HistoryTab`, `usePatientEntries`) |
17
17
  | "Find symbols by name" | `search name` | Functions, types, interfaces, classes. Use `--kind variable` for consts |
18
18
  | "What files depend on this file?" | `rdeps file` | Importers — bare name works |
@@ -21,7 +21,7 @@ All commands are sub-commands of `scip-cli`. Run from the project root.
21
21
  ## Gotchas
22
22
 
23
23
  - **Bare names** resolve functions, types (aliases + interfaces), and classes. Consts/variables need `def --kind variable X` or `search --kind variable X`. Class methods need `members ClassName`, not bare `def methodName`.
24
- - **Ambiguous types** (e.g. `Opts` in multiple hooks) — `def` returns all; `refs` picks the first and warns. Use `search` with a more specific pattern to disambiguate.
24
+ - **Ambiguous types** (e.g. `Opts` in multiple hooks) — `def` returns all matches; `refs` returns refs for all matching symbols. Use `--limit N` to cap results, or use `search` with a more specific pattern to disambiguate.
25
25
  - **First run** in a project may auto-index (one-time wait, ~10-30s for large codebases). JS-only projects (no `tsconfig.json`) are supported automatically.
26
26
 
27
27
  ## Details
@@ -29,43 +29,53 @@ All commands are sub-commands of `scip-cli`. Run from the project root.
29
29
  ### def
30
30
 
31
31
  ```bash
32
- def [--kind <kind>] <symbol>
32
+ def [--kind <kind>] [--limit N] <symbol>
33
33
  ```
34
34
 
35
35
  Kinds: `function`, `method`, `class`, `property`, `variable` — use `--kind` when the bare name isn't in the default set above.
36
36
 
37
+ Default `--limit` is 10. Use `--limit 0` for unlimited (not recommended for large codebases).
38
+
37
39
  ### refs
38
40
 
39
41
  ```bash
40
- refs <symbol>
42
+ refs [--limit N] <symbol>
41
43
  ```
42
44
 
43
45
  Returns `file:line` for each reference. Reads source files to find exact line numbers.
44
46
 
47
+ Default `--limit` is 10. When multiple symbols match, refs are grouped by symbol with `# <symbol>` headers.
48
+
45
49
  ### search
46
50
 
47
51
  ```bash
48
- search [--kind <kind>] <pattern>
52
+ search [--kind <kind>] [--limit N] <pattern>
49
53
  ```
50
54
 
51
55
  Returns `file:line Kind symbolName`. Filters noisy symbols (file-level, parameters, type literals).
52
56
 
57
+ Default `--limit` is 10.
58
+
53
59
  ### symbols
54
60
 
55
61
  ```bash
56
- symbols <file>
62
+ symbols [--limit N] <file>
57
63
  ```
58
64
 
59
65
  Returns `startLine-endLine kind name` for each symbol in the file.
60
66
 
67
+ Default `--limit` is 10.
68
+
61
69
  ### rdeps
62
70
 
63
71
  ```bash
64
- rdeps <file>
72
+ rdeps [--limit N] <file>
65
73
  ```
66
74
 
67
75
  Returns list of files that import from this file.
68
76
 
77
+ Default `--limit` is 10.
78
+
69
79
  ### members
70
80
 
71
81
  ```bash
@@ -1,2 +1,2 @@
1
1
  """scip-cli: Fast code intelligence via SCIP indexes."""
2
- __version__ = "1.0.3"
2
+ __version__ = "1.1.0"
@@ -1,11 +1,23 @@
1
1
  """CLI entry point for scip-cli."""
2
2
  import argparse
3
+ import logging
4
+ import os
3
5
  import sys
4
6
 
5
7
  from . import __version__
6
8
  from .lib import SymbolKind
7
9
  from .commands import refs, def_cmd, search, symbols, rdeps, members, skill, reindex
8
10
 
11
+ # Set up debug logging based on SCIP_CLI_DEBUG env var
12
+ if os.environ.get("SCIP_CLI_DEBUG"):
13
+ logging.basicConfig(
14
+ level=logging.DEBUG,
15
+ format="%(name)s: %(message)s",
16
+ stream=sys.stderr
17
+ )
18
+ else:
19
+ logging.disable(logging.DEBUG)
20
+
9
21
 
10
22
  def main():
11
23
  parser = argparse.ArgumentParser(
@@ -18,23 +30,28 @@ def main():
18
30
  # refs
19
31
  refs_parser = subparsers.add_parser("refs", help="Find references to a symbol")
20
32
  refs_parser.add_argument("symbol", help="Symbol name")
33
+ refs_parser.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
21
34
 
22
35
  # def
23
36
  def_parser = subparsers.add_parser("def", help="Find symbol definition")
24
37
  def_parser.add_argument("--kind", choices=SymbolKind.filterable_values(), help="Filter by kind")
38
+ def_parser.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
25
39
  def_parser.add_argument("symbol", help="Symbol name")
26
40
 
27
41
  # search
28
42
  search_parser = subparsers.add_parser("search", help="Search symbols by pattern")
29
43
  search_parser.add_argument("--kind", choices=SymbolKind.filterable_values(), help="Filter by kind")
44
+ search_parser.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
30
45
  search_parser.add_argument("pattern", help="Search pattern")
31
46
 
32
47
  # symbols
33
48
  symbols_parser = subparsers.add_parser("symbols", help="List symbols in a file")
49
+ symbols_parser.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
34
50
  symbols_parser.add_argument("file", help="File path or pattern")
35
51
 
36
52
  # rdeps
37
53
  rdeps_parser = subparsers.add_parser("rdeps", help="Find reverse dependencies of a file")
54
+ rdeps_parser.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
38
55
  rdeps_parser.add_argument("file", help="File path or pattern")
39
56
 
40
57
  # members
@@ -7,6 +7,7 @@ from ..lib import (
7
7
  read_source_lines,
8
8
  infer_kind,
9
9
  get_def_location,
10
+ format_line_range,
10
11
  )
11
12
 
12
13
 
@@ -14,11 +15,15 @@ def main(args):
14
15
  """Find the definition of a symbol."""
15
16
  db, project_root = setup()
16
17
  try:
17
- symbols = resolve_symbol(db, args.symbol, args.kind)
18
+ limit = args.limit
19
+ symbols = resolve_symbol(db, args.symbol, args.kind, limit=limit + 1)
18
20
  if not symbols:
19
21
  print(f"Symbol '{args.symbol}' not found", file=sys.stderr)
20
22
  sys.exit(1)
21
23
 
24
+ hit_limit = len(symbols) > limit
25
+ symbols = symbols[:limit]
26
+
22
27
  for symbol_id, symbol_str, display_name in symbols:
23
28
  row = get_def_location(db, symbol_id)
24
29
  if not row:
@@ -33,7 +38,10 @@ def main(args):
33
38
  else:
34
39
  source_snippet = ''.join(lines).rstrip('\n')
35
40
 
36
- print(f"{rel_path}:{start_line + 1}:{end_line + 1}")
41
+ print(f"{rel_path}:{format_line_range(start_line, end_line)}")
37
42
  print(source_snippet)
43
+
44
+ if hit_limit:
45
+ print(f"# Warning: more than {limit} symbols match, showing first {limit}", file=sys.stderr)
38
46
  finally:
39
47
  db.close()
@@ -10,6 +10,7 @@ from ..lib import (
10
10
  infer_kind,
11
11
  extract_leaf_name,
12
12
  read_source_lines,
13
+ format_line_range,
13
14
  SymbolKind,
14
15
  )
15
16
 
@@ -79,7 +80,7 @@ def main(args):
79
80
  end_line = start_line
80
81
  break
81
82
 
82
- line_info = f"{start_line + 1}:{end_line + 1}" if start_line is not None else "??"
83
+ line_info = format_line_range(start_line, end_line)
83
84
  print(f"{line_info} {kind} {short}")
84
85
  finally:
85
86
  db.close()
@@ -13,6 +13,7 @@ def main(args):
13
13
  """Find all files that import from this file."""
14
14
  db, _ = setup()
15
15
  try:
16
+ limit = args.limit
16
17
  file_path = resolve_one_file(db, args.file)
17
18
 
18
19
  symbols = get_file_symbols(db, file_path)
@@ -33,7 +34,14 @@ def main(args):
33
34
  print(f"No reverse dependencies found for '{file_path}'", file=sys.stderr)
34
35
  sys.exit(1)
35
36
 
36
- for dep_path in sorted(rdeps):
37
+ sorted_rdeps = sorted(rdeps)
38
+ hit_limit = len(sorted_rdeps) > limit
39
+ sorted_rdeps = sorted_rdeps[:limit]
40
+
41
+ for dep_path in sorted_rdeps:
37
42
  print(dep_path)
43
+
44
+ if hit_limit:
45
+ print(f"# Warning: more than {limit} reverse dependencies, showing first {limit}", file=sys.stderr)
38
46
  finally:
39
47
  db.close()
@@ -0,0 +1,116 @@
1
+ """refs command - find all references to a symbol."""
2
+ import sys
3
+
4
+ from ..lib import (
5
+ setup,
6
+ resolve_symbol,
7
+ read_source_lines,
8
+ extract_leaf_name,
9
+ )
10
+
11
+
12
+ def get_exact_refs(db, symbol_id, project_root, max_refs):
13
+ """Get references with exact line numbers by reading source files."""
14
+ sym_row = db.execute("SELECT symbol FROM global_symbols WHERE id = ?", (symbol_id,)).fetchone()
15
+ if not sym_row:
16
+ return [], False
17
+
18
+ leaf = extract_leaf_name(sym_row[0])
19
+
20
+ chunks = db.execute("""
21
+ SELECT c.id, c.document_id, c.start_line, c.end_line, d.relative_path
22
+ FROM mentions m
23
+ JOIN chunks c ON m.chunk_id = c.id
24
+ JOIN documents d ON c.document_id = d.id
25
+ WHERE m.symbol_id = ? AND m.role != 1
26
+ LIMIT ?
27
+ """, (symbol_id, max_refs + 1)).fetchall()
28
+
29
+ hit_limit = len(chunks) > max_refs
30
+ if hit_limit:
31
+ chunks = chunks[:max_refs]
32
+
33
+ if not chunks:
34
+ return [], False
35
+
36
+ by_doc = {}
37
+ for chunk_id, doc_id, start_line, end_line, rel_path in chunks:
38
+ if doc_id not in by_doc:
39
+ by_doc[doc_id] = {'path': rel_path, 'chunks': []}
40
+ by_doc[doc_id]['chunks'].append((chunk_id, start_line, end_line))
41
+
42
+ results = []
43
+
44
+ for doc_id, info in by_doc.items():
45
+ rel_path = info['path']
46
+ chunks_list = info['chunks']
47
+ if not chunks_list:
48
+ continue
49
+
50
+ min_line = min(c[1] for c in chunks_list)
51
+ max_line = max(c[2] for c in chunks_list)
52
+
53
+ lines = read_source_lines(project_root, rel_path, min_line, max_line)
54
+ if lines is None:
55
+ for chunk_id, start_line, end_line in chunks_list:
56
+ if start_line is not None:
57
+ results.append((rel_path, start_line + 1))
58
+ continue
59
+
60
+ for chunk_id, start_line, end_line in chunks_list:
61
+ if start_line is None:
62
+ continue
63
+ offset = min_line
64
+ found = False
65
+ for line_idx in range(start_line - offset, min(end_line - offset + 1, len(lines))):
66
+ if leaf in lines[line_idx]:
67
+ results.append((rel_path, line_idx + offset + 1))
68
+ found = True
69
+ break
70
+ if not found:
71
+ results.append((rel_path, start_line + 1))
72
+
73
+ return results, hit_limit
74
+
75
+
76
+ def main(args):
77
+ """Find all references to a symbol."""
78
+ db, project_root = setup()
79
+ try:
80
+ limit = args.limit
81
+
82
+ # Get symbols with LIMIT + 1 to detect if we hit the limit
83
+ symbols = resolve_symbol(db, args.symbol, limit=limit + 1)
84
+ if not symbols:
85
+ print(f"Symbol '{args.symbol}' not found", file=sys.stderr)
86
+ sys.exit(1)
87
+
88
+ symbols_hit_limit = len(symbols) > limit
89
+ symbols = symbols[:limit]
90
+
91
+ all_refs = []
92
+ for symbol_id, symbol_str, display_name in symbols:
93
+ refs, refs_hit_limit = get_exact_refs(db, symbol_id, project_root, limit)
94
+ if refs:
95
+ all_refs.append((symbol_str, refs, refs_hit_limit))
96
+
97
+ if not all_refs:
98
+ print(f"No references found for '{args.symbol}'", file=sys.stderr)
99
+ sys.exit(1)
100
+
101
+ if symbols_hit_limit:
102
+ print(f"# Warning: more than {limit} symbols match, showing first {limit}", file=sys.stderr)
103
+
104
+ for symbol_str, refs, refs_hit_limit in all_refs:
105
+ if len(all_refs) > 1:
106
+ print(f"# {symbol_str}")
107
+ if refs_hit_limit:
108
+ print(f"# Warning: more than {limit} refs for this symbol")
109
+ seen = set()
110
+ for path, line in refs:
111
+ key = (path, line)
112
+ if key not in seen:
113
+ seen.add(key)
114
+ print(f"{path}:{line}")
115
+ finally:
116
+ db.close()
@@ -78,6 +78,7 @@ def main(args):
78
78
  """Search symbols by pattern."""
79
79
  db, _ = setup()
80
80
  try:
81
+ limit = args.limit
81
82
  # Escape LIKE wildcards in user pattern
82
83
  escaped_pattern = escape_like(args.pattern)
83
84
 
@@ -95,15 +96,18 @@ def main(args):
95
96
  rows = [r for r in rows if infer_kind(r[1]) == args.kind]
96
97
 
97
98
  # Apply LIMIT after filtering
98
- rows = rows[:100]
99
+ hit_limit = len(rows) > limit
100
+ rows = rows[:limit]
99
101
  else:
100
- rows = db.execute("""
102
+ rows = db.execute(f"""
101
103
  SELECT gs.id, gs.symbol, gs.display_name, der.start_line
102
104
  FROM global_symbols gs
103
105
  LEFT JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
104
106
  WHERE gs.symbol LIKE ? ESCAPE '\\'
105
- LIMIT 100
107
+ LIMIT {limit + 1}
106
108
  """, (f"%{escaped_pattern}%",)).fetchall()
109
+ hit_limit = len(rows) > limit
110
+ rows = rows[:limit]
107
111
 
108
112
  if not rows:
109
113
  if args.kind:
@@ -127,5 +131,8 @@ def main(args):
127
131
 
128
132
  kind_display = kind_to_display(kind)
129
133
  print(f"{file_path}:{line} {kind_display} {symbol_name}")
134
+
135
+ if hit_limit:
136
+ print(f"# Warning: more than {limit} results, showing first {limit}", file=sys.stderr)
130
137
  finally:
131
138
  db.close()
@@ -7,6 +7,7 @@ from ..lib import (
7
7
  get_file_symbols,
8
8
  infer_kind,
9
9
  extract_leaf_name,
10
+ format_line_range,
10
11
  )
11
12
 
12
13
 
@@ -16,20 +17,24 @@ def main(args):
16
17
  try:
17
18
  file_path = resolve_one_file(db, args.file)
18
19
 
19
- symbols = get_file_symbols(db, file_path)
20
+ limit = args.limit
21
+ symbols = get_file_symbols(db, file_path, limit=limit + 1)
20
22
  if not symbols:
21
23
  print(f"No symbols found in '{file_path}'", file=sys.stderr)
22
24
  sys.exit(1)
23
25
 
26
+ hit_limit = len(symbols) > limit
27
+ symbols = symbols[:limit]
28
+
24
29
  for symbol_id, symbol_str, display_name, start_line, end_line in symbols:
25
30
  if symbol_str.endswith('/'):
26
31
  continue
27
32
  kind = infer_kind(symbol_str)
28
33
  short = extract_leaf_name(symbol_str)
29
- if start_line is not None and end_line is not None:
30
- line_info = f"{start_line + 1}-{end_line + 1}"
31
- else:
32
- line_info = "??"
34
+ line_info = format_line_range(start_line, end_line, sep="-")
33
35
  print(f"{line_info} {kind} {short}")
36
+
37
+ if hit_limit:
38
+ print(f"# Warning: more than {limit} symbols, showing first {limit}", file=sys.stderr)
34
39
  finally:
35
40
  db.close()
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """Core library for scip-cli: indexing, symbol resolution, and source reading."""
3
3
  import hashlib
4
+ import logging
4
5
  import os
5
6
  import sqlite3
6
7
  import subprocess
@@ -9,6 +10,15 @@ import tempfile
9
10
  from enum import Enum
10
11
  from pathlib import Path
11
12
 
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def _debug_execute(db, sql, params=()):
17
+ """Execute SQL with optional debug logging."""
18
+ if logger.isEnabledFor(logging.DEBUG):
19
+ logger.debug("SQL: %s | params: %s", sql.strip()[:200], params)
20
+ return db.execute(sql, params)
21
+
12
22
 
13
23
  INDEX_TIMEOUT = 300
14
24
 
@@ -189,7 +199,7 @@ def infer_kind(symbol):
189
199
  return SymbolKind.UNKNOWN
190
200
 
191
201
 
192
- def resolve_symbol(db, name, kind_filter=None):
202
+ def resolve_symbol(db, name, kind_filter=None, limit=None):
193
203
  """Resolve bare name to symbol_id(s).
194
204
 
195
205
  Two-phase resolution: exact leaf match first, then substring fallback.
@@ -198,27 +208,34 @@ def resolve_symbol(db, name, kind_filter=None):
198
208
  db: sqlite3 connection
199
209
  name: bare symbol name (e.g., "useDictation")
200
210
  kind_filter: optional kind filter ('function', 'class', etc)
211
+ limit: optional limit for results (no limit if None)
201
212
 
202
213
  Returns:
203
214
  List of (symbol_id, symbol, display_name) tuples
204
215
  """
205
216
  escaped = escape_like(name)
206
- rows = db.execute("""
217
+
218
+ # Build LIMIT clause
219
+ limit_clause = f"LIMIT {limit}" if limit else ""
220
+
221
+ sql = f"""
207
222
  SELECT id, symbol, display_name FROM global_symbols
208
223
  WHERE symbol LIKE ? ESCAPE '\\' OR symbol LIKE ? ESCAPE '\\' OR symbol LIKE ? ESCAPE '\\'
209
- """, (
224
+ {limit_clause}
225
+ """
226
+ params = (
210
227
  f"%/{escaped}().",
211
228
  f"%/{escaped}#",
212
229
  f"%/{escaped}.",
213
- )).fetchall()
230
+ )
231
+ rows = _debug_execute(db, sql, params).fetchall()
214
232
 
215
233
  results = list(rows)
216
234
 
217
235
  if not results:
218
- rows = db.execute(
219
- "SELECT id, symbol, display_name FROM global_symbols WHERE symbol LIKE ? ESCAPE '\\'",
220
- (f"%{escaped}%",)
221
- ).fetchall()
236
+ sql = f"SELECT id, symbol, display_name FROM global_symbols WHERE symbol LIKE ? ESCAPE '\\' {limit_clause}"
237
+ params = (f"%{escaped}%",)
238
+ rows = _debug_execute(db, sql, params).fetchall()
222
239
  results = [r for r in rows if name in r[1].split("/")[-1]]
223
240
 
224
241
  if kind_filter and results:
@@ -237,7 +254,7 @@ def resolve_file(db, file_pattern):
237
254
  Returns:
238
255
  List of matching relative_paths
239
256
  """
240
- rows = db.execute(
257
+ rows = _debug_execute(db,
241
258
  "SELECT relative_path FROM documents WHERE relative_path = ?",
242
259
  (file_pattern,)
243
260
  ).fetchall()
@@ -251,27 +268,35 @@ def resolve_file(db, file_pattern):
251
268
  else:
252
269
  pattern = escaped.replace("*", "%")
253
270
 
254
- rows = db.execute(
271
+ rows = _debug_execute(db,
255
272
  "SELECT relative_path FROM documents WHERE relative_path LIKE ? ESCAPE '\\'",
256
273
  (pattern,)
257
274
  ).fetchall()
258
275
  return [r[0] for r in rows]
259
276
 
260
277
 
261
- def get_file_symbols(db, relative_path):
278
+ def get_file_symbols(db, relative_path, limit=None):
262
279
  """Get all symbols defined in a file.
263
280
 
281
+ Args:
282
+ db: sqlite3 connection
283
+ relative_path: file path
284
+ limit: optional limit (no limit if None)
285
+
264
286
  Returns:
265
287
  List of (symbol_id, symbol, display_name, start_line, end_line) tuples
266
288
  """
267
- return db.execute("""
289
+ limit_clause = f"LIMIT {limit}" if limit else ""
290
+ sql = f"""
268
291
  SELECT gs.id, gs.symbol, gs.display_name, der.start_line, der.end_line
269
292
  FROM global_symbols gs
270
293
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
271
294
  JOIN documents d ON der.document_id = d.id
272
295
  WHERE d.relative_path = ?
273
296
  ORDER BY der.start_line
274
- """, (relative_path,)).fetchall()
297
+ {limit_clause}
298
+ """
299
+ return _debug_execute(db, sql, (relative_path,)).fetchall()
275
300
 
276
301
 
277
302
  def get_refs_for_symbols(db, symbol_ids):
@@ -284,7 +309,7 @@ def get_refs_for_symbols(db, symbol_ids):
284
309
  return {}
285
310
 
286
311
  placeholders = ','.join('?' * len(symbol_ids))
287
- rows = db.execute(f"""
312
+ rows = _debug_execute(db, f"""
288
313
  SELECT m.symbol_id, d.relative_path, c.start_line
289
314
  FROM mentions m
290
315
  JOIN chunks c ON m.chunk_id = c.id
@@ -308,12 +333,12 @@ def get_members(db, symbol_id):
308
333
  List of (symbol_id, symbol, display_name, start_line, end_line) tuples.
309
334
  Function parameters are already filtered out.
310
335
  """
311
- row = db.execute("SELECT symbol FROM global_symbols WHERE id = ?", (symbol_id,)).fetchone()
336
+ row = _debug_execute(db, "SELECT symbol FROM global_symbols WHERE id = ?", (symbol_id,)).fetchone()
312
337
  if not row:
313
338
  return []
314
339
  parent_symbol = row[0]
315
340
 
316
- rows = db.execute("""
341
+ rows = _debug_execute(db, """
317
342
  SELECT gs.id, gs.symbol, gs.display_name, der.start_line, der.end_line
318
343
  FROM global_symbols gs
319
344
  LEFT JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
@@ -324,7 +349,7 @@ def get_members(db, symbol_id):
324
349
  if not rows:
325
350
  # Escape LIKE metacharacters in parent_symbol before using as prefix
326
351
  escaped_parent = escape_like(parent_symbol)
327
- rows = db.execute("""
352
+ rows = _debug_execute(db, """
328
353
  SELECT gs.id, gs.symbol, gs.display_name, der.start_line, der.end_line
329
354
  FROM global_symbols gs
330
355
  LEFT JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
@@ -341,7 +366,7 @@ def get_def_location(db, symbol_id):
341
366
  Returns:
342
367
  Tuple of (relative_path, start_line, end_line) or None
343
368
  """
344
- return db.execute("""
369
+ return _debug_execute(db, """
345
370
  SELECT d.relative_path, der.start_line, der.end_line
346
371
  FROM defn_enclosing_ranges der
347
372
  JOIN documents d ON der.document_id = d.id
@@ -408,6 +433,24 @@ def warn_ambiguous(name, matches, context="symbol"):
408
433
  print(f"Ambiguous {context} '{name}' ({len(matches)} matches). Using first match: {label}", file=sys.stderr)
409
434
 
410
435
 
436
+ def format_line_range(start_line, end_line, sep=":"):
437
+ """Format a line range as a string, handling None values.
438
+
439
+ Args:
440
+ start_line: 0-indexed start line, or None
441
+ end_line: 0-indexed end line (inclusive), or None
442
+ sep: separator between values (default ":" for "start:end")
443
+
444
+ Returns:
445
+ Formatted string like "10:20" or "??" if unavailable
446
+ """
447
+ if start_line is not None and end_line is not None:
448
+ return f"{start_line + 1}{sep}{end_line + 1}"
449
+ if start_line is not None:
450
+ return f"{start_line + 1}{sep}?"
451
+ return "??"
452
+
453
+
411
454
  def setup():
412
455
  """Setup command execution: find project root and get DB connection.
413
456
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scip-cli
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: Fast code intelligence via SCIP indexes
5
5
  Home-page: https://github.com/flesler/scip-cli
6
6
  Author: Ariel Flesler
@@ -40,10 +40,27 @@ Fast code intelligence CLI for TypeScript/JavaScript and Python projects. Query
40
40
 
41
41
  ### 1. Install scip-cli
42
42
 
43
+ **From PyPI:**
44
+
43
45
  ```bash
44
46
  pip install scip-cli
45
47
  ```
46
48
 
49
+ **From source (local development):**
50
+
51
+ ```bash
52
+ git clone https://github.com/flesler/scip-cli.git
53
+ cd scip-cli
54
+ pip install .
55
+ ```
56
+
57
+ For editable development (where `pip install -e .` fails due to permissions):
58
+
59
+ ```bash
60
+ export PYTHONPATH=/path/to/scip-cli:$PYTHONPATH
61
+ python -m scip_cli --help
62
+ ```
63
+
47
64
  ### 2. Install prerequisites (optional)
48
65
 
49
66
  scip-cli can automatically download the required indexing tools when needed, or you can install them globally for faster performance:
@@ -156,6 +173,19 @@ scip_cli/
156
173
  └── skill.py
157
174
  ```
158
175
 
176
+ ## Development
177
+
178
+ ### Debug Logging
179
+
180
+ Set `SCIP_CLI_DEBUG=1` to enable SQL query logging to stderr:
181
+
182
+ ```bash
183
+ SCIP_CLI_DEBUG=1 scip-cli refs MyFunction
184
+ # Shows: SQL: SELECT ... | params: (...)
185
+ ```
186
+
187
+ This is useful for testing and debugging SQL queries without exposing a `--debug` flag to users.
188
+
159
189
  ## License
160
190
 
161
191
  MIT
@@ -3,8 +3,9 @@ import pytest
3
3
  import sqlite3
4
4
  import tempfile
5
5
  from pathlib import Path
6
- from scip_cli.lib import extract_leaf_name, infer_kind, escape_like, resolve_symbol, resolve_file, read_source_lines, detect_language, SymbolKind
6
+ from scip_cli.lib import extract_leaf_name, infer_kind, escape_like, resolve_symbol, resolve_file, read_source_lines, detect_language, SymbolKind, format_line_range
7
7
  from scip_cli.commands.search import parse_symbol, is_noisy_symbol
8
+ from scip_cli.commands.refs import get_exact_refs
8
9
 
9
10
 
10
11
  class TestDetectLanguage:
@@ -391,3 +392,141 @@ class TestReadSourceLines:
391
392
 
392
393
  lines = read_source_lines(project_root, "nonexistent.ts")
393
394
  assert lines is None
395
+
396
+
397
+ class TestFormatLineRange:
398
+ def test_both_defined(self):
399
+ assert format_line_range(0, 10) == "1:11"
400
+ assert format_line_range(9, 15) == "10:16"
401
+
402
+ def test_only_start_defined(self):
403
+ assert format_line_range(5, None) == "6:?"
404
+
405
+ def test_neither_defined(self):
406
+ assert format_line_range(None, None) == "??"
407
+ assert format_line_range(None, 10) == "??"
408
+
409
+ def test_custom_separator(self):
410
+ assert format_line_range(0, 10, sep="-") == "1-11"
411
+ assert format_line_range(None, None, sep="_") == "??"
412
+
413
+
414
+ class TestGetExactRefs:
415
+ """Tests for get_exact_refs function in refs command."""
416
+
417
+ def _create_test_db(self):
418
+ """Create a test database with minimal schema."""
419
+ conn = sqlite3.connect(":memory:")
420
+ conn.executescript("""
421
+ CREATE TABLE global_symbols (
422
+ id INTEGER PRIMARY KEY,
423
+ symbol TEXT,
424
+ display_name TEXT
425
+ );
426
+ CREATE TABLE documents (
427
+ id INTEGER PRIMARY KEY,
428
+ relative_path TEXT
429
+ );
430
+ CREATE TABLE chunks (
431
+ id INTEGER PRIMARY KEY,
432
+ document_id INTEGER,
433
+ start_line INTEGER,
434
+ end_line INTEGER
435
+ );
436
+ CREATE TABLE mentions (
437
+ id INTEGER PRIMARY KEY,
438
+ symbol_id INTEGER,
439
+ chunk_id INTEGER,
440
+ role INTEGER
441
+ );
442
+ """)
443
+ return conn
444
+
445
+ def test_no_symbol(self):
446
+ """Test when symbol doesn't exist."""
447
+ conn = self._create_test_db()
448
+ refs, hit_limit = get_exact_refs(conn, 999, "/tmp", 10)
449
+ assert refs == []
450
+ assert hit_limit is False
451
+ conn.close()
452
+
453
+ def test_no_mentions(self):
454
+ """Test when symbol exists but has no references."""
455
+ conn = self._create_test_db()
456
+ conn.execute(
457
+ "INSERT INTO global_symbols (id, symbol, display_name) VALUES (?, ?, ?)",
458
+ (1, "scip-python test/test `test.py`/foo().", "foo")
459
+ )
460
+ conn.commit()
461
+ refs, hit_limit = get_exact_refs(conn, 1, "/tmp", 10)
462
+ assert refs == []
463
+ assert hit_limit is False
464
+ conn.close()
465
+
466
+ def test_single_reference(self):
467
+ """Test with a single reference."""
468
+ with tempfile.TemporaryDirectory() as tmpdir:
469
+ conn = self._create_test_db()
470
+ # Create test file
471
+ test_file = Path(tmpdir) / "test.py"
472
+ test_file.write_text("def foo():\n pass\n\nfoo()\n")
473
+
474
+ # Insert data
475
+ conn.execute(
476
+ "INSERT INTO global_symbols (id, symbol, display_name) VALUES (?, ?, ?)",
477
+ (1, "scip-python test/test `test.py`/foo().", "foo")
478
+ )
479
+ conn.execute(
480
+ "INSERT INTO documents (id, relative_path) VALUES (?, ?)",
481
+ (1, "test.py")
482
+ )
483
+ conn.execute(
484
+ "INSERT INTO chunks (id, document_id, start_line, end_line) VALUES (?, ?, ?, ?)",
485
+ (1, 1, 3, 3)
486
+ )
487
+ conn.execute(
488
+ "INSERT INTO mentions (symbol_id, chunk_id, role) VALUES (?, ?, ?)",
489
+ (1, 1, 0)
490
+ )
491
+ conn.commit()
492
+
493
+ refs, hit_limit = get_exact_refs(conn, 1, tmpdir, 10)
494
+ assert len(refs) == 1
495
+ assert refs[0] == ("test.py", 4) # Line 4 (1-indexed)
496
+ assert hit_limit is False
497
+ conn.close()
498
+
499
+ def test_max_refs_limit(self):
500
+ """Test that max_refs limit is respected."""
501
+ with tempfile.TemporaryDirectory() as tmpdir:
502
+ conn = self._create_test_db()
503
+ # Create test file
504
+ test_file = Path(tmpdir) / "test.py"
505
+ test_file.write_text("foo()\nfoo()\nfoo()\n")
506
+
507
+ # Insert symbol
508
+ conn.execute(
509
+ "INSERT INTO global_symbols (id, symbol, display_name) VALUES (?, ?, ?)",
510
+ (1, "scip-python test/test `test.py`/foo().", "foo")
511
+ )
512
+ conn.execute(
513
+ "INSERT INTO documents (id, relative_path) VALUES (?, ?)",
514
+ (1, "test.py")
515
+ )
516
+ # Insert 3 mentions
517
+ for i in range(3):
518
+ conn.execute(
519
+ "INSERT INTO chunks (id, document_id, start_line, end_line) VALUES (?, ?, ?, ?)",
520
+ (i + 1, 1, i, i)
521
+ )
522
+ conn.execute(
523
+ "INSERT INTO mentions (symbol_id, chunk_id, role) VALUES (?, ?, ?)",
524
+ (1, i + 1, 0)
525
+ )
526
+ conn.commit()
527
+
528
+ # Limit to 2 refs
529
+ refs, hit_limit = get_exact_refs(conn, 1, tmpdir, 2)
530
+ assert len(refs) == 2
531
+ assert hit_limit is True
532
+ conn.close()
@@ -1,83 +0,0 @@
1
- """refs command - find all references to a symbol."""
2
- import sys
3
-
4
- from ..lib import (
5
- setup,
6
- resolve_one_symbol,
7
- read_source_lines,
8
- extract_leaf_name,
9
- )
10
-
11
-
12
- def get_exact_refs(db, symbol_id, project_root):
13
- """Get references with exact line numbers by reading source files."""
14
- sym_row = db.execute("SELECT symbol FROM global_symbols WHERE id = ?", (symbol_id,)).fetchone()
15
- if not sym_row:
16
- return []
17
-
18
- leaf = extract_leaf_name(sym_row[0])
19
-
20
- chunks = db.execute("""
21
- SELECT c.id, c.document_id, c.start_line, c.end_line, d.relative_path
22
- FROM mentions m
23
- JOIN chunks c ON m.chunk_id = c.id
24
- JOIN documents d ON c.document_id = d.id
25
- WHERE m.symbol_id = ? AND m.role != 1
26
- """, (symbol_id,)).fetchall()
27
-
28
- if not chunks:
29
- return []
30
-
31
- by_doc = {}
32
- for chunk_id, doc_id, start_line, end_line, rel_path in chunks:
33
- if doc_id not in by_doc:
34
- by_doc[doc_id] = {'path': rel_path, 'chunks': []}
35
- by_doc[doc_id]['chunks'].append((chunk_id, start_line, end_line))
36
-
37
- results = []
38
-
39
- for doc_id, info in by_doc.items():
40
- rel_path = info['path']
41
- min_line = min(c[1] for c in info['chunks'])
42
- max_line = max(c[2] for c in info['chunks'])
43
-
44
- lines = read_source_lines(project_root, rel_path, min_line, max_line)
45
- if lines is None:
46
- for chunk_id, start_line, end_line in info['chunks']:
47
- results.append((rel_path, start_line + 1))
48
- continue
49
-
50
- for chunk_id, start_line, end_line in info['chunks']:
51
- offset = min_line
52
- found = False
53
- for line_idx in range(start_line - offset, min(end_line - offset + 1, len(lines))):
54
- if leaf in lines[line_idx]:
55
- results.append((rel_path, line_idx + offset + 1))
56
- found = True
57
- break
58
- if not found:
59
- results.append((rel_path, start_line + 1))
60
-
61
- return results
62
-
63
-
64
- def main(args):
65
- """Find all references to a symbol."""
66
- db, project_root = setup()
67
- try:
68
- symbol_id, _, _ = resolve_one_symbol(db, args.symbol)
69
-
70
- refs = get_exact_refs(db, symbol_id, project_root)
71
-
72
- if not refs:
73
- print(f"No references found for '{args.symbol}'", file=sys.stderr)
74
- sys.exit(1)
75
-
76
- seen = set()
77
- for path, line in refs:
78
- key = (path, line)
79
- if key not in seen:
80
- seen.add(key)
81
- print(f"{path}:{line}")
82
- finally:
83
- db.close()
File without changes
File without changes
File without changes
File without changes
File without changes