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.
- {scip_cli-1.0.3/scip_cli.egg-info → scip_cli-1.1.0}/PKG-INFO +31 -1
- {scip_cli-1.0.3 → scip_cli-1.1.0}/README.md +30 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/SKILL.md +18 -8
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/__init__.py +1 -1
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/__main__.py +17 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/def_cmd.py +10 -2
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/members.py +2 -1
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/rdeps.py +9 -1
- scip_cli-1.1.0/scip_cli/commands/refs.py +116 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/search.py +10 -3
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/symbols.py +10 -5
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/lib.py +61 -18
- {scip_cli-1.0.3 → scip_cli-1.1.0/scip_cli.egg-info}/PKG-INFO +31 -1
- {scip_cli-1.0.3 → scip_cli-1.1.0}/tests/test_pure_functions.py +140 -1
- scip_cli-1.0.3/scip_cli/commands/refs.py +0 -83
- {scip_cli-1.0.3 → scip_cli-1.1.0}/LICENSE +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/MANIFEST.in +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/pyproject.toml +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/__init__.py +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/reindex.py +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli/commands/skill.py +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli.egg-info/SOURCES.txt +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli.egg-info/dependency_links.txt +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli.egg-info/entry_points.txt +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/scip_cli.egg-info/top_level.txt +0 -0
- {scip_cli-1.0.3 → scip_cli-1.1.0}/setup.cfg +0 -0
- {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
|
+
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.
|
|
15
|
-
| "Where is X used/called?" | `refs X` | All file:line locations
|
|
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`
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
230
|
+
)
|
|
231
|
+
rows = _debug_execute(db, sql, params).fetchall()
|
|
214
232
|
|
|
215
233
|
results = list(rows)
|
|
216
234
|
|
|
217
235
|
if not results:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|