scip-cli 2.1.0__tar.gz → 2.2.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-2.1.0 → scip_cli-2.2.0}/.cursor/rules/agent.mdc +33 -10
- {scip_cli-2.1.0 → scip_cli-2.2.0}/PKG-INFO +1 -1
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/__init__.py +1 -1
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/analyze/common.py +26 -3
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/analyze/file.py +53 -17
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/analyze/graph.py +4 -1
- scip_cli-2.2.0/scip_cli/analyze/live.py +139 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/analyze/project.py +103 -28
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/analyze.py +3 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/refs.py +4 -2
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/output.py +17 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/queries.py +21 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/symbols.py +26 -0
- scip_cli-2.2.0/scripts/publish.sh +96 -0
- scip_cli-2.2.0/scripts/verify_rovetia_reports.py +128 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/analyze_db.py +63 -0
- scip_cli-2.2.0/tests/fixture_catalog.py +44 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/domain/i18n/en.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/domain/i18n/index.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/domain/i18n/useLocale.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/domain/labels/index.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/events/streamTypes.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/gateway/index.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/hooks/useHookA.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/hooks/useHookB.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/hooks/useItems.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/integrations/inferenceClient.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/loaders/useInference.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/pages/panelPage.ts +4 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/rules/applyRule.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/types/a.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/types/b.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/ui/Button.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/ui/LazyPanel.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/ui/buttonConsumer.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/ui/menuModule.ts +1 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/widgets/OrphanWidget.ts +3 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_analyze.py +38 -4
- scip_cli-2.2.0/tests/test_analyze_graph.py +66 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_e2e.py +2 -2
- scip_cli-2.2.0/tests/test_e2e_analyze_patterns.py +176 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_pure_functions.py +9 -1
- scip_cli-2.1.0/scripts/publish.sh +0 -32
- scip_cli-2.1.0/tests/fixture_catalog.py +0 -17
- scip_cli-2.1.0/tests/test_analyze_graph.py +0 -30
- {scip_cli-2.1.0 → scip_cli-2.2.0}/.gitignore +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/.pre-commit-config.yaml +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/LICENSE +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/README.md +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/pyproject.toml +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/SKILL.md +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/__main__.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/analyze/__init__.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/analyze/sections.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/analyze/symbol.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/analyze/targets.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/cache.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/cli_args.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/__init__.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/code.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/members.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/rdeps.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/reindex.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/search.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/skill.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/commands/symbols.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/config.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/debug.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/discover.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/indexing.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/merge.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/paths.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/project.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/scip_tool.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/scope.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/session.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/source.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/sql.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scip_cli/targets.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scripts/build.sh +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/scripts/test.sh +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/__init__.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/conftest.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/e2e_harness.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/package.json +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/app/handler.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/config.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/consumer.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/dead.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/helper.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/index.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/user.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/widget.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/tsconfig.json +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/perf_util.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_analyze_perf.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_cache.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_composability.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_config.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_discover.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_e2e_perf.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_index_prune.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_indexer_env.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_merge.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_multi_symbol.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_qualified_symbols.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_reindex.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_scip_tool.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_scope.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_targets.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.0}/tests/test_typescript_projects.py +0 -0
|
@@ -31,7 +31,7 @@ Node.js + `npx` required for integration tests (`scip-typescript`). Optional: `S
|
|
|
31
31
|
| Task | Command |
|
|
32
32
|
|------|---------|
|
|
33
33
|
| Full suite | `pytest` |
|
|
34
|
-
| E2e loop | `pytest tests/test_e2e.py` |
|
|
34
|
+
| E2e loop | `pytest tests/test_e2e.py tests/test_e2e_analyze_patterns.py` |
|
|
35
35
|
| Lint / format | `ruff check .` / `ruff format .` |
|
|
36
36
|
|
|
37
37
|
Pre-commit: ruff + ruff-format + full pytest. E2e before commit on command changes.
|
|
@@ -69,9 +69,10 @@ When you or a review subagent finds a repo issue — bug, dead code, cycle, risk
|
|
|
69
69
|
|
|
70
70
|
If **yes** (the signal is in `documents`, `mentions`, `global_symbols`, `defn_enclosing_ranges`):
|
|
71
71
|
|
|
72
|
-
1. **
|
|
73
|
-
2. **
|
|
74
|
-
3. **
|
|
72
|
+
1. **Red on the fixture** — minimal generic repro + e2e test (see **TDD → External project feedback**). Do not name real customer repos in tests.
|
|
73
|
+
2. **Extend `analyze`** — new or tuned SQL/heuristic in `scip_cli/analyze/`, priority in `sections.py`; unit tests only for graph/SQL plumbing (`analyze_db.py`, `test_analyze_graph.py`).
|
|
74
|
+
3. **Dogfood** — `reindex` on this repo (or re-run fixture e2e) → confirm signal appears or false positive is filtered.
|
|
75
|
+
4. **Then** fix production code in the other repo (or document a known blind spot).
|
|
75
76
|
|
|
76
77
|
Only skip step 1 when the issue is **outside the index** (runtime, CLI UX, docs drift, git, env, concurrency, test harness).
|
|
77
78
|
|
|
@@ -137,14 +138,36 @@ The e2e fixture and this repo's own index are **small** — every query looks fa
|
|
|
137
138
|
|
|
138
139
|
## TDD
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
2. **Green** — `scip_cli/commands/*.py`.
|
|
142
|
-
3. **Refactor** — unit tests for pure helpers only.
|
|
141
|
+
### Default: extend the shared fixture
|
|
143
142
|
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
**One indexed TS project** — `tests/fixtures/sample-project/` — indexed once per session via `indexed_fixture` (`conftest.py`). Stable names live in `tests/fixture_catalog.py`.
|
|
144
|
+
|
|
145
|
+
1. **Red** — add a **minimal** `.ts` file (or tweak an existing one) that reproduces the SCIP shape; assert in `tests/test_e2e.py` or `tests/test_e2e_analyze_patterns.py` using `cli` / `open_index_db` + analyze helpers.
|
|
146
|
+
2. **Green** — fix `scip_cli/` (commands, `analyze/*`, `queries.py`, …).
|
|
147
|
+
3. **Refactor** — only if a pure helper deserves isolation.
|
|
148
|
+
|
|
149
|
+
- **E2e** (`cli` / `indexed_fixture`): in-process `main()` or direct Python API on the **real** index — default for behavior.
|
|
146
150
|
- **No subprocess** for CLI tests; no `reindex` in e2e (mutates `~/.cache/scip-cli`).
|
|
147
|
-
- New symbols:
|
|
151
|
+
- New symbols/paths: fixture file + `fixture_catalog.py` constant — never hardcode paths in test bodies.
|
|
152
|
+
|
|
153
|
+
### External project feedback → fixture (mandatory workflow)
|
|
154
|
+
|
|
155
|
+
When dogfooding **another repo** surfaces a bug or false positive (analyze noise, dead-export mistake, cycle, stale type, refs mismatch):
|
|
156
|
+
|
|
157
|
+
1. **Capture the pattern, not the project** — note the SCIP shape (module `/`, export alias `foo0:`, lazy import, `Props#`, barrel cycle, …). Do **not** copy real repo names, paths, or filenames into tests or docs.
|
|
158
|
+
2. **Red on the fixture** — smallest TS repro under `tests/fixtures/sample-project/` with generic names (`LazyPanel`, `evictItem`, `OrphanWidget`, …). E2e test proves the bug against **real `scip-typescript`** output.
|
|
159
|
+
3. **Green** — fix analyze heuristics / SQL / CLI; dogfood on this repo if applicable.
|
|
160
|
+
4. **Verify** — `pytest tests/test_e2e_analyze_patterns.py` (or full `pytest`); only then trim redundant tests.
|
|
161
|
+
|
|
162
|
+
**Prefer fixture e2e over fake SQLite graphs.** `tests/analyze_db.py` (`AnalyzeDbBuilder`, `:memory:`) is for SQL/dashboard **plumbing** and graph primitives (`test_analyze_graph.py`, `mini_codebase_db`) — not for reproducing production SCIP symbol shapes. Hand-built symbols often diverge from scip-typescript (path format, `0:` aliases, module rows) and can pass while production still breaks.
|
|
163
|
+
|
|
164
|
+
| Use fixture e2e | Use `analyze_db` unit |
|
|
165
|
+
|-----------------|----------------------|
|
|
166
|
+
| False positives / analyze checks from real projects | Tarjan, edge-type SQL, `run_all` section wiring |
|
|
167
|
+
| Any behavior that depends on indexer symbol strings | Noise filters with one or two synthetic rows |
|
|
168
|
+
| CLI commands (`refs`, `search`, `analyze` output) | Merge/index prune tests |
|
|
169
|
+
|
|
170
|
+
If both layers help during development, **keep both until e2e passes**, then delete duplicate in-memory repros — fixture is the source of truth.
|
|
148
171
|
|
|
149
172
|
## Codebase map (contributors)
|
|
150
173
|
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import re
|
|
6
6
|
|
|
7
7
|
from ..sql import debug_execute
|
|
8
|
-
from ..symbols import extract_leaf_name
|
|
8
|
+
from ..symbols import extract_leaf_name, is_module_symbol
|
|
9
9
|
|
|
10
10
|
DEFAULT_LIMIT = 20
|
|
11
11
|
|
|
@@ -25,8 +25,14 @@ def fetch_one(db, sql: str, params=()):
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def short_name(symbol: str) -> str:
|
|
28
|
+
if is_module_symbol(symbol):
|
|
29
|
+
return "(module)"
|
|
28
30
|
leaf = extract_leaf_name(symbol)
|
|
29
|
-
|
|
31
|
+
if leaf:
|
|
32
|
+
return leaf
|
|
33
|
+
if symbol.endswith("/"):
|
|
34
|
+
return "(module)"
|
|
35
|
+
return symbol.split("/")[-1][:60]
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
def is_test_path(relative_path: str) -> bool:
|
|
@@ -53,10 +59,19 @@ def is_cli_entrypoint(relative_path: str, symbol: str) -> bool:
|
|
|
53
59
|
return path == "scip_cli/__main__.py" or "/commands/" in path
|
|
54
60
|
|
|
55
61
|
|
|
62
|
+
def is_generated_analyze_path(relative_path: str) -> bool:
|
|
63
|
+
p = relative_path.replace("\\", "/")
|
|
64
|
+
if "/types/prisma/" in p:
|
|
65
|
+
return True
|
|
66
|
+
return p.endswith("types/resolvers.ts")
|
|
67
|
+
|
|
68
|
+
|
|
56
69
|
def analyze_noise(relative_path: str, symbol: str, *, include_tests: bool = False) -> bool:
|
|
57
70
|
"""True for rows that clutter analyze dashboards (test paths, module-private helpers)."""
|
|
58
71
|
if not include_tests and is_test_path(relative_path):
|
|
59
72
|
return True
|
|
73
|
+
if is_generated_analyze_path(relative_path):
|
|
74
|
+
return True
|
|
60
75
|
if short_name(symbol).startswith("_"):
|
|
61
76
|
return True
|
|
62
77
|
if is_cli_entrypoint(relative_path, symbol):
|
|
@@ -80,12 +95,20 @@ def is_analyze_dashboard_export(relative_path: str, symbol: str) -> bool:
|
|
|
80
95
|
return "()." in symbol or name.endswith(")")
|
|
81
96
|
|
|
82
97
|
|
|
98
|
+
def is_component_props_type(symbol: str) -> bool:
|
|
99
|
+
"""React-style Props interfaces — rarely referenced outside the component file."""
|
|
100
|
+
name = short_name(symbol)
|
|
101
|
+
return bool(name and name.endswith("Props") and "#" in symbol.split("/")[-1])
|
|
102
|
+
|
|
103
|
+
|
|
83
104
|
def stale_type_noise(relative_path: str, symbol: str, consumers: int) -> bool:
|
|
84
105
|
"""Dataclass-style types with no SCIP consumers (typing-only)."""
|
|
106
|
+
if is_component_props_type(symbol):
|
|
107
|
+
return True
|
|
85
108
|
if consumers > 0:
|
|
86
109
|
return False
|
|
87
110
|
name = short_name(symbol)
|
|
88
|
-
if not name or not name[0].isupper():
|
|
111
|
+
if not name or name == "(module)" or not name[0].isupper():
|
|
89
112
|
return False
|
|
90
113
|
path = relative_path.replace("\\", "/")
|
|
91
114
|
return path.endswith(("config.py", "scope.py", "analyze/targets.py"))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from .common import DEFAULT_LIMIT, SYM_DEF_JOIN, analyze_noise, fetch_all, short_name
|
|
6
|
+
from .live import LiveIndex
|
|
6
7
|
from .sections import Check, Priority, run_checks
|
|
7
8
|
from .symbol import symbol_pressure
|
|
8
9
|
|
|
@@ -82,10 +83,11 @@ def file_consumers(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[s
|
|
|
82
83
|
|
|
83
84
|
|
|
84
85
|
def unreferenced_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
86
|
+
live = LiveIndex(db)
|
|
85
87
|
rows = fetch_all(
|
|
86
88
|
db,
|
|
87
89
|
"""
|
|
88
|
-
SELECT gs.symbol, der.start_line, der.end_line
|
|
90
|
+
SELECT gs.symbol, der.start_line, der.end_line, def_d.id
|
|
89
91
|
FROM global_symbols gs
|
|
90
92
|
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
91
93
|
JOIN documents def_d ON der.document_id = def_d.id
|
|
@@ -104,14 +106,18 @@ def unreferenced_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) ->
|
|
|
104
106
|
""",
|
|
105
107
|
(relative_path, limit),
|
|
106
108
|
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
lines = []
|
|
110
|
+
for symbol, start, end, def_doc_id in rows:
|
|
111
|
+
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
112
|
+
continue
|
|
113
|
+
if live.dead_export_noise(symbol, def_doc_id):
|
|
114
|
+
continue
|
|
115
|
+
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
116
|
+
return lines
|
|
112
117
|
|
|
113
118
|
|
|
114
119
|
def same_file_only_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
120
|
+
live = LiveIndex(db)
|
|
115
121
|
rows = fetch_all(
|
|
116
122
|
db,
|
|
117
123
|
"""
|
|
@@ -135,18 +141,22 @@ def same_file_only_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -
|
|
|
135
141
|
""",
|
|
136
142
|
(relative_path, limit),
|
|
137
143
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
lines = []
|
|
145
|
+
for symbol, start, end in rows:
|
|
146
|
+
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
147
|
+
continue
|
|
148
|
+
if live.same_file_export_noise(symbol):
|
|
149
|
+
continue
|
|
150
|
+
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
151
|
+
return lines
|
|
143
152
|
|
|
144
153
|
|
|
145
154
|
def dead_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
155
|
+
live = LiveIndex(db)
|
|
146
156
|
rows = fetch_all(
|
|
147
157
|
db,
|
|
148
158
|
"""
|
|
149
|
-
SELECT gs.symbol, der.start_line, der.end_line
|
|
159
|
+
SELECT gs.symbol, der.start_line, der.end_line, def_d.id
|
|
150
160
|
FROM global_symbols gs
|
|
151
161
|
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
152
162
|
JOIN documents def_d ON der.document_id = def_d.id
|
|
@@ -164,11 +174,14 @@ def dead_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str
|
|
|
164
174
|
""",
|
|
165
175
|
(relative_path, limit),
|
|
166
176
|
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
lines = []
|
|
178
|
+
for symbol, start, end, def_doc_id in rows:
|
|
179
|
+
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
180
|
+
continue
|
|
181
|
+
if live.dead_export_noise(symbol, def_doc_id):
|
|
182
|
+
continue
|
|
183
|
+
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
184
|
+
return lines
|
|
172
185
|
|
|
173
186
|
|
|
174
187
|
def imports_summary(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
@@ -240,6 +253,29 @@ def coupling_for(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str
|
|
|
240
253
|
return [f"{other} shared={shared}" for other, shared in rows]
|
|
241
254
|
|
|
242
255
|
|
|
256
|
+
def count_file_importers(db, relative_path: str) -> int:
|
|
257
|
+
from ..queries import get_file_symbols, get_importer_paths
|
|
258
|
+
|
|
259
|
+
symbols = get_file_symbols(db, relative_path)
|
|
260
|
+
if not symbols:
|
|
261
|
+
return 0
|
|
262
|
+
symbol_ids = [row[0] for row in symbols]
|
|
263
|
+
return len(get_importer_paths(db, symbol_ids, relative_path))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def dead_export_rdeps_warning(db, relative_path: str, *, limit: int = DEFAULT_LIMIT) -> str | None:
|
|
267
|
+
"""Warn when dead/unreferenced exports coexist with file importers (SCIP symbol split)."""
|
|
268
|
+
if not dead_in_file(db, relative_path, limit=limit) and not unreferenced_in_file(db, relative_path, limit=limit):
|
|
269
|
+
return None
|
|
270
|
+
importer_count = count_file_importers(db, relative_path)
|
|
271
|
+
if importer_count == 0:
|
|
272
|
+
return None
|
|
273
|
+
return (
|
|
274
|
+
f"Warning: {relative_path} has {importer_count} importer(s) but lists dead/unreferenced exports — "
|
|
275
|
+
"default-export, lazy(), or object-alias false positives are likely; verify with rg or rdeps"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
243
279
|
def top_symbol_pressure(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
244
280
|
"""Pressure metrics for the most-consumed exports in a file."""
|
|
245
281
|
cap = min(5, limit)
|
|
@@ -4,16 +4,19 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
|
|
7
|
+
from ..symbols import cycle_runtime_edge_sql
|
|
7
8
|
from .common import fetch_all
|
|
8
9
|
|
|
9
|
-
FILE_EDGES_SQL = """
|
|
10
|
+
FILE_EDGES_SQL = f"""
|
|
10
11
|
SELECT DISTINCT d1.relative_path AS from_file, d2.relative_path AS to_file
|
|
11
12
|
FROM mentions m
|
|
12
13
|
JOIN chunks c ON m.chunk_id = c.id
|
|
13
14
|
JOIN documents d1 ON c.document_id = d1.id
|
|
14
15
|
JOIN defn_enclosing_ranges der ON m.symbol_id = der.symbol_id
|
|
15
16
|
JOIN documents d2 ON der.document_id = d2.id
|
|
17
|
+
JOIN global_symbols gs ON gs.id = der.symbol_id
|
|
16
18
|
WHERE d1.id != d2.id AND m.role != 1
|
|
19
|
+
AND {cycle_runtime_edge_sql()}
|
|
17
20
|
"""
|
|
18
21
|
|
|
19
22
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Heuristics for SCIP symbols that look dead but are live via module or export-alias indirection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from ..sql import debug_execute
|
|
8
|
+
from ..symbols import extract_leaf_name, is_module_symbol
|
|
9
|
+
|
|
10
|
+
_EXTERNAL_MENTION = """
|
|
11
|
+
SELECT 1 FROM mentions m
|
|
12
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
13
|
+
WHERE m.symbol_id = ? AND m.role != 1 AND c.document_id != ?
|
|
14
|
+
LIMIT 1
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fetch_all(db, sql: str, params=()) -> list:
|
|
19
|
+
return debug_execute(db, sql, params).fetchall()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _fetch_one(db, sql: str, params=()):
|
|
23
|
+
return debug_execute(db, sql, params).fetchone()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_export_alias_symbol(symbol: str) -> bool:
|
|
27
|
+
tail = symbol.split("/")[-1]
|
|
28
|
+
return tail.endswith("0:") and "()." not in tail
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def export_alias_base(symbol: str) -> str | None:
|
|
32
|
+
if not is_export_alias_symbol(symbol):
|
|
33
|
+
return None
|
|
34
|
+
tail = symbol.split("/")[-1]
|
|
35
|
+
return tail[:-2]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def export_value_base(symbol: str) -> str | None:
|
|
39
|
+
"""Base name for a default-exported function or class/type definition."""
|
|
40
|
+
if symbol.endswith("()."):
|
|
41
|
+
return extract_leaf_name(symbol) or None
|
|
42
|
+
if symbol.endswith("#") and "#typeLiteral" not in symbol and "()." not in symbol.split("/")[-1]:
|
|
43
|
+
leaf = extract_leaf_name(symbol)
|
|
44
|
+
return leaf or None
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _def_doc_id_from_symbol(db, symbol: str) -> int | None:
|
|
49
|
+
match = re.search(r"`([^`]+)`", symbol)
|
|
50
|
+
if not match:
|
|
51
|
+
return None
|
|
52
|
+
filename = match.group(1)
|
|
53
|
+
row = _fetch_one(
|
|
54
|
+
db,
|
|
55
|
+
"SELECT id FROM documents WHERE relative_path GLOB ? OR relative_path = ? LIMIT 1",
|
|
56
|
+
(f"*/{filename}", filename),
|
|
57
|
+
)
|
|
58
|
+
return row[0] if row else None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LiveIndex:
|
|
62
|
+
"""Precomputed live-module docs and export-alias bases for one analyze pass."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, db) -> None:
|
|
65
|
+
self.live_module_docs: set[int] = set()
|
|
66
|
+
self.live_alias_bases: set[str] = set()
|
|
67
|
+
self._build(db)
|
|
68
|
+
|
|
69
|
+
def _build(self, db) -> None:
|
|
70
|
+
for (doc_id,) in _fetch_all(
|
|
71
|
+
db,
|
|
72
|
+
"""
|
|
73
|
+
SELECT DISTINCT der.document_id
|
|
74
|
+
FROM global_symbols gs
|
|
75
|
+
JOIN defn_enclosing_ranges der ON der.symbol_id = gs.id
|
|
76
|
+
WHERE gs.symbol LIKE '%/' AND gs.symbol NOT LIKE '%().'
|
|
77
|
+
AND EXISTS (
|
|
78
|
+
SELECT 1 FROM mentions m
|
|
79
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
80
|
+
WHERE m.symbol_id = gs.id AND m.role != 1 AND c.document_id != der.document_id
|
|
81
|
+
)
|
|
82
|
+
""",
|
|
83
|
+
):
|
|
84
|
+
self.live_module_docs.add(doc_id)
|
|
85
|
+
|
|
86
|
+
for sym_id, symbol in _fetch_all(
|
|
87
|
+
db,
|
|
88
|
+
"""
|
|
89
|
+
SELECT id, symbol FROM global_symbols
|
|
90
|
+
WHERE symbol LIKE '%0:' AND symbol NOT LIKE '%().'
|
|
91
|
+
""",
|
|
92
|
+
):
|
|
93
|
+
base = export_alias_base(symbol)
|
|
94
|
+
if not base:
|
|
95
|
+
continue
|
|
96
|
+
def_doc = _def_doc_id_from_symbol(db, symbol)
|
|
97
|
+
if def_doc is None:
|
|
98
|
+
continue
|
|
99
|
+
if _fetch_one(db, _EXTERNAL_MENTION, (sym_id, def_doc)):
|
|
100
|
+
self.live_alias_bases.add(base)
|
|
101
|
+
|
|
102
|
+
def dead_export_noise(self, symbol: str, def_doc_id: int) -> bool:
|
|
103
|
+
if is_module_symbol(symbol):
|
|
104
|
+
return True
|
|
105
|
+
base = export_value_base(symbol)
|
|
106
|
+
if base and base in self.live_alias_bases:
|
|
107
|
+
return True
|
|
108
|
+
return bool(base and def_doc_id in self.live_module_docs)
|
|
109
|
+
|
|
110
|
+
def same_file_export_noise(self, symbol: str) -> bool:
|
|
111
|
+
"""Export used only in-file but live via module or export alias elsewhere."""
|
|
112
|
+
base = export_value_base(symbol)
|
|
113
|
+
if not base:
|
|
114
|
+
return False
|
|
115
|
+
return base in self.live_alias_bases
|
|
116
|
+
|
|
117
|
+
def stale_type_live_noise(self, symbol: str, def_doc_id: int) -> bool:
|
|
118
|
+
"""Class/type looks unused but file is live via default export or module import."""
|
|
119
|
+
base = export_value_base(symbol)
|
|
120
|
+
if not base:
|
|
121
|
+
return False
|
|
122
|
+
if base in self.live_alias_bases:
|
|
123
|
+
return True
|
|
124
|
+
return def_doc_id in self.live_module_docs
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def has_same_file_reference_usage(db, symbol_id: int, def_doc_id: int) -> bool:
|
|
128
|
+
"""Type or value referenced in the same file (extends, signatures, object literal)."""
|
|
129
|
+
row = _fetch_one(
|
|
130
|
+
db,
|
|
131
|
+
"""
|
|
132
|
+
SELECT 1 FROM mentions m
|
|
133
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
134
|
+
WHERE m.symbol_id = ? AND m.role = 0 AND c.document_id = ?
|
|
135
|
+
LIMIT 1
|
|
136
|
+
""",
|
|
137
|
+
(symbol_id, def_doc_id),
|
|
138
|
+
)
|
|
139
|
+
return row is not None
|
|
@@ -17,6 +17,7 @@ from .common import (
|
|
|
17
17
|
stale_type_noise,
|
|
18
18
|
)
|
|
19
19
|
from .graph import FILE_EDGES_SQL, fetch_file_edges, find_longer_cycles
|
|
20
|
+
from .live import LiveIndex, has_same_file_reference_usage
|
|
20
21
|
from .sections import Check, Priority, run_checks
|
|
21
22
|
|
|
22
23
|
|
|
@@ -149,6 +150,26 @@ def cycles(
|
|
|
149
150
|
return lines[:limit]
|
|
150
151
|
|
|
151
152
|
|
|
153
|
+
def _format_dead_export_rows(
|
|
154
|
+
db,
|
|
155
|
+
rows,
|
|
156
|
+
live: LiveIndex,
|
|
157
|
+
*,
|
|
158
|
+
include_tests: bool,
|
|
159
|
+
limit: int,
|
|
160
|
+
) -> list[str]:
|
|
161
|
+
lines = []
|
|
162
|
+
for symbol, path, loc, def_doc_id in rows:
|
|
163
|
+
if analyze_noise(path, symbol, include_tests=include_tests):
|
|
164
|
+
continue
|
|
165
|
+
if live.dead_export_noise(symbol, def_doc_id):
|
|
166
|
+
continue
|
|
167
|
+
lines.append(f"{short_name(symbol)} loc={loc} ({path})")
|
|
168
|
+
if len(lines) >= limit:
|
|
169
|
+
break
|
|
170
|
+
return lines
|
|
171
|
+
|
|
172
|
+
|
|
152
173
|
def stale_types(
|
|
153
174
|
db,
|
|
154
175
|
limit: int = DEFAULT_LIMIT,
|
|
@@ -160,7 +181,7 @@ def stale_types(
|
|
|
160
181
|
rows = fetch_all(
|
|
161
182
|
db,
|
|
162
183
|
f"""
|
|
163
|
-
SELECT gs.symbol, def_d.relative_path,
|
|
184
|
+
SELECT gs.id, gs.symbol, def_d.relative_path, def_d.id,
|
|
164
185
|
COUNT(DISTINCT CASE WHEN ref_d.id != def_d.id THEN ref_d.id END) AS consumers
|
|
165
186
|
FROM global_symbols gs
|
|
166
187
|
{SYM_DEF_JOIN}
|
|
@@ -177,13 +198,21 @@ def stale_types(
|
|
|
177
198
|
""",
|
|
178
199
|
(*scope_params, limit * 5),
|
|
179
200
|
)
|
|
180
|
-
lines = [
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
201
|
+
lines = []
|
|
202
|
+
live = LiveIndex(db)
|
|
203
|
+
for sym_id, symbol, path, def_doc_id, consumers in rows:
|
|
204
|
+
if analyze_noise(path, symbol, include_tests=include_tests):
|
|
205
|
+
continue
|
|
206
|
+
if stale_type_noise(path, symbol, consumers):
|
|
207
|
+
continue
|
|
208
|
+
if consumers == 0 and has_same_file_reference_usage(db, sym_id, def_doc_id):
|
|
209
|
+
continue
|
|
210
|
+
if live.stale_type_live_noise(symbol, def_doc_id):
|
|
211
|
+
continue
|
|
212
|
+
lines.append(f"{short_name(symbol)} consumers={consumers} ({path})")
|
|
213
|
+
if len(lines) >= limit:
|
|
214
|
+
break
|
|
215
|
+
return lines
|
|
187
216
|
|
|
188
217
|
|
|
189
218
|
def unreferenced_symbols(
|
|
@@ -198,7 +227,8 @@ def unreferenced_symbols(
|
|
|
198
227
|
db,
|
|
199
228
|
f"""
|
|
200
229
|
SELECT gs.symbol, def_d.relative_path,
|
|
201
|
-
sym_def.end_line - sym_def.start_line + 1 AS loc
|
|
230
|
+
sym_def.end_line - sym_def.start_line + 1 AS loc,
|
|
231
|
+
def_d.id
|
|
202
232
|
FROM global_symbols gs
|
|
203
233
|
{SYM_DEF_JOIN}
|
|
204
234
|
WHERE NOT EXISTS (
|
|
@@ -215,12 +245,7 @@ def unreferenced_symbols(
|
|
|
215
245
|
""",
|
|
216
246
|
(*scope_params, limit * 5),
|
|
217
247
|
)
|
|
218
|
-
|
|
219
|
-
f"{short_name(symbol)} loc={loc} ({path})"
|
|
220
|
-
for symbol, path, loc in rows
|
|
221
|
-
if not analyze_noise(path, symbol, include_tests=include_tests)
|
|
222
|
-
]
|
|
223
|
-
return lines[:limit]
|
|
248
|
+
return _format_dead_export_rows(db, rows, LiveIndex(db), include_tests=include_tests, limit=limit)
|
|
224
249
|
|
|
225
250
|
|
|
226
251
|
def same_file_only(
|
|
@@ -231,6 +256,7 @@ def same_file_only(
|
|
|
231
256
|
scope: str | None = None,
|
|
232
257
|
) -> list[str]:
|
|
233
258
|
scope_clause, scope_params = path_filter_sql(db, scope, doc_alias="def_d")
|
|
259
|
+
live = LiveIndex(db)
|
|
234
260
|
rows = fetch_all(
|
|
235
261
|
db,
|
|
236
262
|
f"""
|
|
@@ -253,12 +279,16 @@ def same_file_only(
|
|
|
253
279
|
""",
|
|
254
280
|
(*scope_params, limit * 5),
|
|
255
281
|
)
|
|
256
|
-
lines = [
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
282
|
+
lines = []
|
|
283
|
+
for symbol, path, loc in rows:
|
|
284
|
+
if analyze_noise(path, symbol, include_tests=include_tests):
|
|
285
|
+
continue
|
|
286
|
+
if live.same_file_export_noise(symbol):
|
|
287
|
+
continue
|
|
288
|
+
lines.append(f"{short_name(symbol)} loc={loc} ({path})")
|
|
289
|
+
if len(lines) >= limit:
|
|
290
|
+
break
|
|
291
|
+
return lines
|
|
262
292
|
|
|
263
293
|
|
|
264
294
|
def symbols_test_only_consumers(
|
|
@@ -316,7 +346,8 @@ def dead_exports(
|
|
|
316
346
|
db,
|
|
317
347
|
f"""
|
|
318
348
|
SELECT gs.symbol, def_d.relative_path,
|
|
319
|
-
sym_def.end_line - sym_def.start_line + 1 AS loc
|
|
349
|
+
sym_def.end_line - sym_def.start_line + 1 AS loc,
|
|
350
|
+
def_d.id
|
|
320
351
|
FROM global_symbols gs
|
|
321
352
|
{SYM_DEF_JOIN}
|
|
322
353
|
WHERE NOT EXISTS (
|
|
@@ -332,12 +363,50 @@ def dead_exports(
|
|
|
332
363
|
""",
|
|
333
364
|
(*scope_params, limit * 5),
|
|
334
365
|
)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
366
|
+
return _format_dead_export_rows(db, rows, LiveIndex(db), include_tests=include_tests, limit=limit)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def possibly_unused_exports(
|
|
370
|
+
db,
|
|
371
|
+
limit: int = DEFAULT_LIMIT,
|
|
372
|
+
*,
|
|
373
|
+
include_tests: bool = False,
|
|
374
|
+
scope: str | None = None,
|
|
375
|
+
) -> list[str]:
|
|
376
|
+
"""Exports with no direct external refs but live via module import or export alias."""
|
|
377
|
+
scope_clause, scope_params = path_filter_sql(db, scope, doc_alias="def_d")
|
|
378
|
+
live = LiveIndex(db)
|
|
379
|
+
rows = fetch_all(
|
|
380
|
+
db,
|
|
381
|
+
f"""
|
|
382
|
+
SELECT gs.symbol, def_d.relative_path,
|
|
383
|
+
sym_def.end_line - sym_def.start_line + 1 AS loc,
|
|
384
|
+
def_d.id
|
|
385
|
+
FROM global_symbols gs
|
|
386
|
+
{SYM_DEF_JOIN}
|
|
387
|
+
WHERE NOT EXISTS (
|
|
388
|
+
SELECT 1
|
|
389
|
+
FROM mentions m
|
|
390
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
391
|
+
WHERE m.symbol_id = gs.id
|
|
392
|
+
AND m.role != 1
|
|
393
|
+
AND c.document_id != def_d.id
|
|
394
|
+
){scope_clause}
|
|
395
|
+
ORDER BY loc DESC, def_d.relative_path
|
|
396
|
+
LIMIT ?
|
|
397
|
+
""",
|
|
398
|
+
(*scope_params, limit * 5),
|
|
399
|
+
)
|
|
400
|
+
lines = []
|
|
401
|
+
for symbol, path, loc, def_doc_id in rows:
|
|
402
|
+
if analyze_noise(path, symbol, include_tests=include_tests):
|
|
403
|
+
continue
|
|
404
|
+
if not live.dead_export_noise(symbol, def_doc_id):
|
|
405
|
+
continue
|
|
406
|
+
lines.append(f"{short_name(symbol)} loc={loc} (live via module/alias) ({path})")
|
|
407
|
+
if len(lines) >= limit:
|
|
408
|
+
break
|
|
409
|
+
return lines
|
|
341
410
|
|
|
342
411
|
|
|
343
412
|
def top_coupling(
|
|
@@ -388,6 +457,12 @@ def run_all(
|
|
|
388
457
|
Check("cycles", Priority.HIGH, f"Cycles (file dependencies){suffix}", cycles),
|
|
389
458
|
Check("unreferenced", Priority.HIGH, f"Unreferenced symbols (no refs){suffix}", unreferenced_symbols),
|
|
390
459
|
Check("dead_exports", Priority.HIGH, f"Dead exports (no external refs){suffix}", dead_exports),
|
|
460
|
+
Check(
|
|
461
|
+
"possibly_unused",
|
|
462
|
+
Priority.MEDIUM,
|
|
463
|
+
f"Possibly unused exports (live via module/alias){suffix}",
|
|
464
|
+
possibly_unused_exports,
|
|
465
|
+
),
|
|
391
466
|
Check("stale_types", Priority.HIGH, f"Stale types (≤1 external consumer){suffix}", stale_types),
|
|
392
467
|
Check("same_file_only", Priority.MEDIUM, f"Same-file only (consider _prefix){suffix}", same_file_only),
|
|
393
468
|
Check(
|
|
@@ -45,6 +45,9 @@ def _project_sections(
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def _file_sections(db, path: str, *, limit: int, priorities) -> list[tuple[str, list[str]]]:
|
|
48
|
+
warn = file_checks.dead_export_rdeps_warning(db, path, limit=limit)
|
|
49
|
+
if warn:
|
|
50
|
+
print(warn, file=sys.stderr)
|
|
48
51
|
return file_checks.run_all(db, path, limit=limit, priorities=priorities)
|
|
49
52
|
|
|
50
53
|
|
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
6
|
from ..cli_args import path_scope_from_args
|
|
7
|
-
from ..output import limit_and_warn, maybe_print_symbol_header, symbol_output_label
|
|
7
|
+
from ..output import limit_and_warn, maybe_print_symbol_header, symbol_output_label, warn_ambiguous_refs
|
|
8
8
|
from ..paths import path_filter_sql, path_in_scope
|
|
9
9
|
from ..queries import resolve_symbol
|
|
10
10
|
from ..session import setup
|
|
@@ -120,7 +120,9 @@ def _resolve_symbol_groups(db, names, limit, path_scope):
|
|
|
120
120
|
if not symbols:
|
|
121
121
|
print(f"Symbol '{query_name}' not found", file=sys.stderr)
|
|
122
122
|
continue
|
|
123
|
-
|
|
123
|
+
trimmed = limit_and_warn(symbols, limit, "symbols")
|
|
124
|
+
warn_ambiguous_refs(query_name, trimmed, db)
|
|
125
|
+
groups.append((query_name, trimmed))
|
|
124
126
|
return groups
|
|
125
127
|
|
|
126
128
|
|