scip-cli 2.2.0__tar.gz → 2.2.1__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.2.0 → scip_cli-2.2.1}/PKG-INFO +2 -2
- {scip_cli-2.2.0 → scip_cli-2.2.1}/README.md +1 -1
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/SKILL.md +3 -1
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/__init__.py +1 -1
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/__main__.py +1 -1
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/analyze/common.py +3 -3
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/analyze/file.py +20 -23
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/analyze/live.py +44 -15
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/analyze/project.py +25 -62
- scip_cli-2.2.1/scip_cli/analyze/sections.py +120 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/analyze/symbol.py +3 -2
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/analyze.py +32 -13
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_analyze.py +39 -18
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_e2e_analyze_patterns.py +0 -8
- scip_cli-2.2.0/scip_cli/analyze/sections.py +0 -77
- scip_cli-2.2.0/scripts/verify_rovetia_reports.py +0 -128
- {scip_cli-2.2.0 → scip_cli-2.2.1}/.cursor/rules/agent.mdc +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/.gitignore +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/.pre-commit-config.yaml +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/LICENSE +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/pyproject.toml +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/analyze/__init__.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/analyze/graph.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/analyze/targets.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/cache.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/cli_args.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/__init__.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/code.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/members.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/rdeps.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/refs.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/reindex.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/search.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/skill.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/commands/symbols.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/config.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/debug.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/discover.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/indexing.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/merge.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/output.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/paths.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/project.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/queries.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/scip_tool.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/scope.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/session.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/source.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/sql.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/symbols.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scip_cli/targets.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scripts/build.sh +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scripts/publish.sh +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/scripts/test.sh +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/__init__.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/analyze_db.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/conftest.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/e2e_harness.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixture_catalog.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/package.json +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/app/handler.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/config.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/consumer.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/dead.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/domain/i18n/en.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/domain/i18n/index.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/domain/i18n/useLocale.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/domain/labels/index.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/events/streamTypes.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/gateway/index.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/helper.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/hooks/useHookA.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/hooks/useHookB.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/hooks/useItems.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/index.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/integrations/inferenceClient.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/loaders/useInference.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/pages/panelPage.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/rules/applyRule.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/types/a.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/types/b.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/ui/Button.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/ui/LazyPanel.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/ui/buttonConsumer.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/ui/menuModule.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/user.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/widget.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/widgets/OrphanWidget.ts +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/tsconfig.json +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/perf_util.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_analyze_graph.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_analyze_perf.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_cache.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_composability.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_config.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_discover.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_e2e.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_e2e_perf.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_index_prune.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_indexer_env.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_merge.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_multi_symbol.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_pure_functions.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_qualified_symbols.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_reindex.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_scip_tool.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_scope.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_targets.py +0 -0
- {scip_cli-2.2.0 → scip_cli-2.2.1}/tests/test_typescript_projects.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scip-cli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Fast code intelligence via SCIP indexes
|
|
5
5
|
Project-URL: Homepage, https://github.com/flesler/scip-cli
|
|
6
6
|
Author: Ariel Flesler
|
|
@@ -255,7 +255,7 @@ Use `--priority high` for a quick gate; `--priority high,medium` adds context. F
|
|
|
255
255
|
| **Cycles** | Import/mention cycles between production files — break the edge or extract shared code |
|
|
256
256
|
| **Unreferenced** | No usage in the index at all — delete |
|
|
257
257
|
| **Dead exports** | No external refs — delete or `_` prefix |
|
|
258
|
-
| **Stale types** | Classes/types with
|
|
258
|
+
| **Stale types** | Classes/types with no external consumer in the index — verify in-file or type-only use before removing |
|
|
259
259
|
| **Same-file only** | Used only inside defining file — rename to `_` |
|
|
260
260
|
| **Test-only consumers** | Cross-file refs are all from tests — promote to e2e or accept as internal |
|
|
261
261
|
|
|
@@ -235,7 +235,7 @@ Use `--priority high` for a quick gate; `--priority high,medium` adds context. F
|
|
|
235
235
|
| **Cycles** | Import/mention cycles between production files — break the edge or extract shared code |
|
|
236
236
|
| **Unreferenced** | No usage in the index at all — delete |
|
|
237
237
|
| **Dead exports** | No external refs — delete or `_` prefix |
|
|
238
|
-
| **Stale types** | Classes/types with
|
|
238
|
+
| **Stale types** | Classes/types with no external consumer in the index — verify in-file or type-only use before removing |
|
|
239
239
|
| **Same-file only** | Used only inside defining file — rename to `_` |
|
|
240
240
|
| **Test-only consumers** | Cross-file refs are all from tests — promote to e2e or accept as internal |
|
|
241
241
|
|
|
@@ -128,10 +128,12 @@ analyze [--limit N] [--path PATH] [--include-tests] [target]
|
|
|
128
128
|
|
|
129
129
|
Sections are ordered **high → medium → low**. `[high]` cycles, unreferenced, dead exports, stale types; `[medium]` same-file-only, change surface (file); `[low]` test-only consumers (noisy on Python), coupling, bottlenecks, hotspots.
|
|
130
130
|
|
|
131
|
+
`--limit` caps **result rows across the whole run** (default 20); remaining checks are skipped once the cap is reached. `(none)` sections and section headers do not count.
|
|
132
|
+
|
|
131
133
|
`--priority high` or `--priority high,medium` (also `1`/`2`/`3`) skips lower tiers.
|
|
132
134
|
|
|
133
135
|
Directory detection uses the filesystem when present, otherwise an indexed path prefix. `--path` narrows ambiguous file/symbol resolution only (not directory scope — pass the dir as `target`).
|
|
134
136
|
|
|
135
137
|
**Dogfood loop:** `reindex` → `analyze --limit 25` → `analyze scip_cli` or `analyze scip_cli/queries.py` on suspects. Skips test paths in project-wide and directory runs (`tests/`, `*.test.*`, `*.spec.*`); `--include-tests` to include them. File-target analyze always includes that file.
|
|
136
138
|
|
|
137
|
-
**Easy pickings:** **Cycles** and **dead exports** (production paths) — cross-file cleanup. **Stale types** —
|
|
139
|
+
**Easy pickings:** **Cycles** and **dead exports** (production paths) — cross-file cleanup. **Stale types** — types with no external refs in the index. Ignore `analyze/*` section helpers in dead exports. “Dead” = no refs from *other* files in the index, not `vulture`.
|
|
@@ -115,7 +115,7 @@ def main():
|
|
|
115
115
|
help="Omit for project-wide; directory for scoped project + per-file; file or symbol for focused checks",
|
|
116
116
|
)
|
|
117
117
|
add_path_argument(analyze_parser)
|
|
118
|
-
add_limit_argument(analyze_parser, default=20, help_suffix="rows
|
|
118
|
+
add_limit_argument(analyze_parser, default=20, help_suffix="result rows total")
|
|
119
119
|
analyze_parser.add_argument(
|
|
120
120
|
"--include-tests",
|
|
121
121
|
action="store_true",
|
|
@@ -127,7 +127,7 @@ def cycle_path_noise(cycle_line: str, *, include_tests: bool = False) -> bool:
|
|
|
127
127
|
return any(is_test_path(part.strip()) for part in parts if part.strip())
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
def section(title: str, lines: list[str]) -> tuple[str, list[str]]:
|
|
130
|
+
def section(title: str, lines: list[str], *, preface: str | None = None) -> tuple[str, list[str], str | None]:
|
|
131
131
|
if not lines:
|
|
132
|
-
return title, ["(none)"]
|
|
133
|
-
return title, lines
|
|
132
|
+
return title, ["(none)"], None
|
|
133
|
+
return title, lines, preface
|
|
@@ -3,7 +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
|
+
from .live import LiveIndex, file_has_scip_importers, has_same_file_reference_usage
|
|
7
7
|
from .sections import Check, Priority, run_checks
|
|
8
8
|
from .symbol import symbol_pressure
|
|
9
9
|
|
|
@@ -121,7 +121,7 @@ def same_file_only_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -
|
|
|
121
121
|
rows = fetch_all(
|
|
122
122
|
db,
|
|
123
123
|
"""
|
|
124
|
-
SELECT gs.symbol, der.start_line, der.end_line
|
|
124
|
+
SELECT gs.symbol, der.start_line, der.end_line, def_d.id
|
|
125
125
|
FROM global_symbols gs
|
|
126
126
|
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
127
127
|
JOIN documents def_d ON der.document_id = def_d.id
|
|
@@ -142,10 +142,12 @@ def same_file_only_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -
|
|
|
142
142
|
(relative_path, limit),
|
|
143
143
|
)
|
|
144
144
|
lines = []
|
|
145
|
-
for symbol, start, end in rows:
|
|
145
|
+
for symbol, start, end, def_doc_id in rows:
|
|
146
146
|
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
147
147
|
continue
|
|
148
|
-
if live.same_file_export_noise(symbol):
|
|
148
|
+
if live.same_file_export_noise(symbol, def_doc_id):
|
|
149
|
+
continue
|
|
150
|
+
if not file_has_scip_importers(db, relative_path, live=live, def_doc_id=def_doc_id):
|
|
149
151
|
continue
|
|
150
152
|
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
151
153
|
return lines
|
|
@@ -156,7 +158,7 @@ def dead_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str
|
|
|
156
158
|
rows = fetch_all(
|
|
157
159
|
db,
|
|
158
160
|
"""
|
|
159
|
-
SELECT gs.symbol, der.start_line, der.end_line, def_d.id
|
|
161
|
+
SELECT gs.id, gs.symbol, der.start_line, der.end_line, def_d.id
|
|
160
162
|
FROM global_symbols gs
|
|
161
163
|
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
162
164
|
JOIN documents def_d ON der.document_id = def_d.id
|
|
@@ -175,9 +177,11 @@ def dead_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str
|
|
|
175
177
|
(relative_path, limit),
|
|
176
178
|
)
|
|
177
179
|
lines = []
|
|
178
|
-
for symbol, start, end, def_doc_id in rows:
|
|
180
|
+
for sym_id, symbol, start, end, def_doc_id in rows:
|
|
179
181
|
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
180
182
|
continue
|
|
183
|
+
if has_same_file_reference_usage(db, sym_id, def_doc_id):
|
|
184
|
+
continue
|
|
181
185
|
if live.dead_export_noise(symbol, def_doc_id):
|
|
182
186
|
continue
|
|
183
187
|
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
@@ -263,19 +267,6 @@ def count_file_importers(db, relative_path: str) -> int:
|
|
|
263
267
|
return len(get_importer_paths(db, symbol_ids, relative_path))
|
|
264
268
|
|
|
265
269
|
|
|
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
|
-
|
|
279
270
|
def top_symbol_pressure(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
280
271
|
"""Pressure metrics for the most-consumed exports in a file."""
|
|
281
272
|
cap = min(5, limit)
|
|
@@ -357,8 +348,10 @@ def _run_file_checks(
|
|
|
357
348
|
db,
|
|
358
349
|
limit: int,
|
|
359
350
|
priorities,
|
|
360
|
-
|
|
361
|
-
|
|
351
|
+
*,
|
|
352
|
+
budget=None,
|
|
353
|
+
) -> list[tuple[str, list[str], str | None]]:
|
|
354
|
+
return run_checks(checks, db, limit, priorities, budget=budget)
|
|
362
355
|
|
|
363
356
|
|
|
364
357
|
def run_all(
|
|
@@ -366,12 +359,14 @@ def run_all(
|
|
|
366
359
|
relative_path: str,
|
|
367
360
|
limit: int = DEFAULT_LIMIT,
|
|
368
361
|
priorities=None,
|
|
369
|
-
|
|
362
|
+
budget=None,
|
|
363
|
+
) -> list[tuple[str, list[str], str | None]]:
|
|
370
364
|
return _run_file_checks(
|
|
371
365
|
_file_checks(relative_path, include_top_symbols=True),
|
|
372
366
|
db,
|
|
373
367
|
limit,
|
|
374
368
|
priorities,
|
|
369
|
+
budget=budget,
|
|
375
370
|
)
|
|
376
371
|
|
|
377
372
|
|
|
@@ -380,11 +375,13 @@ def run_all_sections_only(
|
|
|
380
375
|
relative_path: str,
|
|
381
376
|
limit: int = DEFAULT_LIMIT,
|
|
382
377
|
priorities=None,
|
|
383
|
-
|
|
378
|
+
budget=None,
|
|
379
|
+
) -> list[tuple[str, list[str], str | None]]:
|
|
384
380
|
"""Per-file sections without top-symbols (directory batch)."""
|
|
385
381
|
return _run_file_checks(
|
|
386
382
|
_file_checks(relative_path, include_top_symbols=False),
|
|
387
383
|
db,
|
|
388
384
|
limit,
|
|
389
385
|
priorities,
|
|
386
|
+
budget=budget,
|
|
390
387
|
)
|
|
@@ -64,24 +64,25 @@ class LiveIndex:
|
|
|
64
64
|
def __init__(self, db) -> None:
|
|
65
65
|
self.live_module_docs: set[int] = set()
|
|
66
66
|
self.live_alias_bases: set[str] = set()
|
|
67
|
+
self.module_importers: dict[int, int] = {}
|
|
67
68
|
self._build(db)
|
|
68
69
|
|
|
69
70
|
def _build(self, db) -> None:
|
|
70
|
-
for
|
|
71
|
+
for doc_id, importer_count in _fetch_all(
|
|
71
72
|
db,
|
|
72
73
|
"""
|
|
73
|
-
SELECT DISTINCT
|
|
74
|
+
SELECT der.document_id, COUNT(DISTINCT c.document_id)
|
|
74
75
|
FROM global_symbols gs
|
|
75
76
|
JOIN defn_enclosing_ranges der ON der.symbol_id = gs.id
|
|
77
|
+
JOIN mentions m ON m.symbol_id = gs.id AND m.role != 1
|
|
78
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
76
79
|
WHERE gs.symbol LIKE '%/' AND gs.symbol NOT LIKE '%().'
|
|
77
|
-
AND
|
|
78
|
-
|
|
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
|
-
)
|
|
80
|
+
AND c.document_id != der.document_id
|
|
81
|
+
GROUP BY der.document_id
|
|
82
82
|
""",
|
|
83
83
|
):
|
|
84
84
|
self.live_module_docs.add(doc_id)
|
|
85
|
+
self.module_importers[doc_id] = importer_count
|
|
85
86
|
|
|
86
87
|
for sym_id, symbol in _fetch_all(
|
|
87
88
|
db,
|
|
@@ -99,20 +100,35 @@ class LiveIndex:
|
|
|
99
100
|
if _fetch_one(db, _EXTERNAL_MENTION, (sym_id, def_doc)):
|
|
100
101
|
self.live_alias_bases.add(base)
|
|
101
102
|
|
|
102
|
-
def
|
|
103
|
+
def possibly_live_label(self, symbol: str, def_doc_id: int) -> str | None:
|
|
104
|
+
"""Proof of liveness via module or export alias (used to suppress false dead-export hits)."""
|
|
103
105
|
if is_module_symbol(symbol):
|
|
104
|
-
|
|
106
|
+
if def_doc_id not in self.live_module_docs:
|
|
107
|
+
return None
|
|
108
|
+
count = self.module_importers.get(def_doc_id, 0)
|
|
109
|
+
return f"module_import:{count}"
|
|
105
110
|
base = export_value_base(symbol)
|
|
106
|
-
if
|
|
107
|
-
return
|
|
108
|
-
|
|
111
|
+
if not base:
|
|
112
|
+
return None
|
|
113
|
+
if base in self.live_alias_bases:
|
|
114
|
+
return "export_alias"
|
|
115
|
+
if def_doc_id in self.live_module_docs:
|
|
116
|
+
count = self.module_importers.get(def_doc_id, 0)
|
|
117
|
+
return f"default_export:{count}"
|
|
118
|
+
return None
|
|
109
119
|
|
|
110
|
-
def
|
|
120
|
+
def dead_export_noise(self, symbol: str, def_doc_id: int) -> bool:
|
|
121
|
+
"""Suppress from dead_exports when live via module or export alias."""
|
|
122
|
+
return self.possibly_live_label(symbol, def_doc_id) is not None or is_module_symbol(symbol)
|
|
123
|
+
|
|
124
|
+
def same_file_export_noise(self, symbol: str, def_doc_id: int | None = None) -> bool:
|
|
111
125
|
"""Export used only in-file but live via module or export alias elsewhere."""
|
|
112
126
|
base = export_value_base(symbol)
|
|
113
127
|
if not base:
|
|
114
128
|
return False
|
|
115
|
-
|
|
129
|
+
if base in self.live_alias_bases:
|
|
130
|
+
return True
|
|
131
|
+
return def_doc_id is not None and def_doc_id in self.live_module_docs
|
|
116
132
|
|
|
117
133
|
def stale_type_live_noise(self, symbol: str, def_doc_id: int) -> bool:
|
|
118
134
|
"""Class/type looks unused but file is live via default export or module import."""
|
|
@@ -125,7 +141,7 @@ class LiveIndex:
|
|
|
125
141
|
|
|
126
142
|
|
|
127
143
|
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,
|
|
144
|
+
"""Type or value referenced in the same file (extends, signatures, handler registration)."""
|
|
129
145
|
row = _fetch_one(
|
|
130
146
|
db,
|
|
131
147
|
"""
|
|
@@ -137,3 +153,16 @@ def has_same_file_reference_usage(db, symbol_id: int, def_doc_id: int) -> bool:
|
|
|
137
153
|
(symbol_id, def_doc_id),
|
|
138
154
|
)
|
|
139
155
|
return row is not None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def file_has_scip_importers(db, relative_path: str, *, live: LiveIndex, def_doc_id: int) -> bool:
|
|
159
|
+
"""True when the index shows another file importing this module or its symbols."""
|
|
160
|
+
if def_doc_id in live.live_module_docs:
|
|
161
|
+
return True
|
|
162
|
+
from ..queries import get_file_symbols, get_importer_paths
|
|
163
|
+
|
|
164
|
+
symbols = get_file_symbols(db, relative_path)
|
|
165
|
+
if not symbols:
|
|
166
|
+
return False
|
|
167
|
+
symbol_ids = [row[0] for row in symbols]
|
|
168
|
+
return bool(get_importer_paths(db, symbol_ids, relative_path))
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import re
|
|
6
6
|
|
|
7
7
|
from ..paths import path_filter_sql, path_filter_sql_any, path_in_scope
|
|
8
|
+
from ..symbols import is_module_symbol
|
|
8
9
|
from .common import (
|
|
9
10
|
DEFAULT_LIMIT,
|
|
10
11
|
SYM_DEF_JOIN,
|
|
@@ -17,7 +18,7 @@ from .common import (
|
|
|
17
18
|
stale_type_noise,
|
|
18
19
|
)
|
|
19
20
|
from .graph import FILE_EDGES_SQL, fetch_file_edges, find_longer_cycles
|
|
20
|
-
from .live import LiveIndex, has_same_file_reference_usage
|
|
21
|
+
from .live import LiveIndex, file_has_scip_importers, has_same_file_reference_usage
|
|
21
22
|
from .sections import Check, Priority, run_checks
|
|
22
23
|
|
|
23
24
|
|
|
@@ -80,7 +81,7 @@ def bottlenecks(
|
|
|
80
81
|
lines = [
|
|
81
82
|
f"{short_name(symbol)} score={score} loc={loc} fan_in={fan_in} fan_out={fan_out} ({path})"
|
|
82
83
|
for symbol, path, fan_in, fan_out, loc, score in rows
|
|
83
|
-
if not analyze_noise(path, symbol, include_tests=include_tests)
|
|
84
|
+
if not analyze_noise(path, symbol, include_tests=include_tests) and not is_module_symbol(symbol)
|
|
84
85
|
]
|
|
85
86
|
return lines[:limit]
|
|
86
87
|
|
|
@@ -114,7 +115,7 @@ def hotspots(
|
|
|
114
115
|
lines = [
|
|
115
116
|
f"{short_name(symbol)} refs={ref_count} files={file_count} ({path})"
|
|
116
117
|
for symbol, path, ref_count, file_count in rows
|
|
117
|
-
if not analyze_noise(path, symbol, include_tests=include_tests)
|
|
118
|
+
if not analyze_noise(path, symbol, include_tests=include_tests) and not is_module_symbol(symbol)
|
|
118
119
|
]
|
|
119
120
|
return lines[:limit]
|
|
120
121
|
|
|
@@ -159,9 +160,11 @@ def _format_dead_export_rows(
|
|
|
159
160
|
limit: int,
|
|
160
161
|
) -> list[str]:
|
|
161
162
|
lines = []
|
|
162
|
-
for symbol, path, loc, def_doc_id in rows:
|
|
163
|
+
for symbol_id, symbol, path, loc, def_doc_id in rows:
|
|
163
164
|
if analyze_noise(path, symbol, include_tests=include_tests):
|
|
164
165
|
continue
|
|
166
|
+
if has_same_file_reference_usage(db, symbol_id, def_doc_id):
|
|
167
|
+
continue
|
|
165
168
|
if live.dead_export_noise(symbol, def_doc_id):
|
|
166
169
|
continue
|
|
167
170
|
lines.append(f"{short_name(symbol)} loc={loc} ({path})")
|
|
@@ -192,7 +195,7 @@ def stale_types(
|
|
|
192
195
|
AND gs.symbol NOT LIKE '%().'
|
|
193
196
|
AND gs.symbol NOT LIKE '%#typeLiteral%'{scope_clause}
|
|
194
197
|
GROUP BY gs.id
|
|
195
|
-
HAVING consumers
|
|
198
|
+
HAVING consumers = 0
|
|
196
199
|
ORDER BY consumers ASC, def_d.relative_path
|
|
197
200
|
LIMIT ?
|
|
198
201
|
""",
|
|
@@ -226,7 +229,7 @@ def unreferenced_symbols(
|
|
|
226
229
|
rows = fetch_all(
|
|
227
230
|
db,
|
|
228
231
|
f"""
|
|
229
|
-
SELECT gs.symbol, def_d.relative_path,
|
|
232
|
+
SELECT gs.id, gs.symbol, def_d.relative_path,
|
|
230
233
|
sym_def.end_line - sym_def.start_line + 1 AS loc,
|
|
231
234
|
def_d.id
|
|
232
235
|
FROM global_symbols gs
|
|
@@ -261,7 +264,8 @@ def same_file_only(
|
|
|
261
264
|
db,
|
|
262
265
|
f"""
|
|
263
266
|
SELECT gs.symbol, def_d.relative_path,
|
|
264
|
-
sym_def.end_line - sym_def.start_line + 1 AS loc
|
|
267
|
+
sym_def.end_line - sym_def.start_line + 1 AS loc,
|
|
268
|
+
def_d.id
|
|
265
269
|
FROM global_symbols gs
|
|
266
270
|
{SYM_DEF_JOIN}
|
|
267
271
|
WHERE EXISTS (
|
|
@@ -280,10 +284,12 @@ def same_file_only(
|
|
|
280
284
|
(*scope_params, limit * 5),
|
|
281
285
|
)
|
|
282
286
|
lines = []
|
|
283
|
-
for symbol, path, loc in rows:
|
|
287
|
+
for symbol, path, loc, def_doc_id in rows:
|
|
284
288
|
if analyze_noise(path, symbol, include_tests=include_tests):
|
|
285
289
|
continue
|
|
286
|
-
if live.same_file_export_noise(symbol):
|
|
290
|
+
if live.same_file_export_noise(symbol, def_doc_id):
|
|
291
|
+
continue
|
|
292
|
+
if not file_has_scip_importers(db, path, live=live, def_doc_id=def_doc_id):
|
|
287
293
|
continue
|
|
288
294
|
lines.append(f"{short_name(symbol)} loc={loc} ({path})")
|
|
289
295
|
if len(lines) >= limit:
|
|
@@ -345,7 +351,7 @@ def dead_exports(
|
|
|
345
351
|
rows = fetch_all(
|
|
346
352
|
db,
|
|
347
353
|
f"""
|
|
348
|
-
SELECT gs.symbol, def_d.relative_path,
|
|
354
|
+
SELECT gs.id, gs.symbol, def_d.relative_path,
|
|
349
355
|
sym_def.end_line - sym_def.start_line + 1 AS loc,
|
|
350
356
|
def_d.id
|
|
351
357
|
FROM global_symbols gs
|
|
@@ -366,49 +372,6 @@ def dead_exports(
|
|
|
366
372
|
return _format_dead_export_rows(db, rows, LiveIndex(db), include_tests=include_tests, limit=limit)
|
|
367
373
|
|
|
368
374
|
|
|
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
|
|
410
|
-
|
|
411
|
-
|
|
412
375
|
def top_coupling(
|
|
413
376
|
db,
|
|
414
377
|
limit: int = DEFAULT_LIMIT,
|
|
@@ -450,21 +413,21 @@ def run_all(
|
|
|
450
413
|
include_tests: bool = False,
|
|
451
414
|
scope: str | None = None,
|
|
452
415
|
priorities=None,
|
|
453
|
-
|
|
416
|
+
budget=None,
|
|
417
|
+
) -> list[tuple[str, list[str], str | None]]:
|
|
454
418
|
suffix = _scope_suffix(scope)
|
|
455
|
-
opts = {"include_tests": include_tests, "scope": scope}
|
|
419
|
+
opts = {"include_tests": include_tests, "scope": scope, "budget": budget}
|
|
456
420
|
checks = [
|
|
457
421
|
Check("cycles", Priority.HIGH, f"Cycles (file dependencies){suffix}", cycles),
|
|
458
422
|
Check("unreferenced", Priority.HIGH, f"Unreferenced symbols (no refs){suffix}", unreferenced_symbols),
|
|
459
|
-
Check("dead_exports", Priority.HIGH, f"Dead exports (no external refs){suffix}", dead_exports),
|
|
460
423
|
Check(
|
|
461
|
-
"
|
|
462
|
-
Priority.
|
|
463
|
-
f"
|
|
464
|
-
|
|
424
|
+
"dead_exports",
|
|
425
|
+
Priority.HIGH,
|
|
426
|
+
f"Dead exports (no in-file or external use){suffix}",
|
|
427
|
+
dead_exports,
|
|
465
428
|
),
|
|
466
|
-
Check("stale_types", Priority.HIGH, f"Stale types (
|
|
467
|
-
Check("same_file_only", Priority.MEDIUM, f"Same-file only (
|
|
429
|
+
Check("stale_types", Priority.HIGH, f"Stale types (no external consumers){suffix}", stale_types),
|
|
430
|
+
Check("same_file_only", Priority.MEDIUM, f"Same-file only (in-file use, not exported){suffix}", same_file_only),
|
|
468
431
|
Check(
|
|
469
432
|
"test_only",
|
|
470
433
|
Priority.LOW,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Priority ordering and filtering for analyze dashboards."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .common import section
|
|
11
|
+
|
|
12
|
+
CheckFn = Callable[..., list[str]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Priority(str, Enum):
|
|
16
|
+
HIGH = "high"
|
|
17
|
+
MEDIUM = "medium"
|
|
18
|
+
LOW = "low"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_ORDER = (Priority.HIGH, Priority.MEDIUM, Priority.LOW)
|
|
22
|
+
_ALIASES = {
|
|
23
|
+
"1": Priority.HIGH,
|
|
24
|
+
"2": Priority.MEDIUM,
|
|
25
|
+
"3": Priority.LOW,
|
|
26
|
+
"h": Priority.HIGH,
|
|
27
|
+
"m": Priority.MEDIUM,
|
|
28
|
+
"l": Priority.LOW,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_priorities(value: str | None) -> set[Priority] | None:
|
|
33
|
+
"""Parse --priority (comma-separated names or 1/2/3). None means all levels."""
|
|
34
|
+
if not value:
|
|
35
|
+
return None
|
|
36
|
+
out: set[Priority] = set()
|
|
37
|
+
for part in value.replace(" ", "").split(","):
|
|
38
|
+
if not part:
|
|
39
|
+
continue
|
|
40
|
+
key = part.lower()
|
|
41
|
+
if key in _ALIASES:
|
|
42
|
+
out.add(_ALIASES[key])
|
|
43
|
+
continue
|
|
44
|
+
try:
|
|
45
|
+
out.add(Priority(key))
|
|
46
|
+
except ValueError as exc:
|
|
47
|
+
allowed = ", ".join(p.value for p in Priority)
|
|
48
|
+
raise RuntimeError(f"unknown analyze priority {part!r} (use {allowed} or 1/2/3)") from exc
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Check:
|
|
54
|
+
key: str
|
|
55
|
+
priority: Priority
|
|
56
|
+
title: str
|
|
57
|
+
run: CheckFn
|
|
58
|
+
false_positive_preface: str | None = None
|
|
59
|
+
|
|
60
|
+
def labeled_title(self) -> str:
|
|
61
|
+
return f"[{self.priority.value}] {self.title}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Shown only when the section has hits (not "(none)").
|
|
65
|
+
FALSE_POSITIVE_PREFACES: dict[str, str] = {
|
|
66
|
+
"dead_exports": (
|
|
67
|
+
"SCIP may miss dynamic loading (loadFiles, GraphQL) and default-export object members "
|
|
68
|
+
"— verify with rdeps/rg before deleting."
|
|
69
|
+
),
|
|
70
|
+
"unreferenced": (
|
|
71
|
+
"No mentions in the index — symbols may still run via dynamic import or side-effect registration."
|
|
72
|
+
),
|
|
73
|
+
"same_file_only": ("Referenced only in the defining file — often handlers or private helpers, not dead exports."),
|
|
74
|
+
"stale_types": ("No cross-file refs in the index — may still be used in-file or as a type-only shape."),
|
|
75
|
+
"cycles": "Remaining cycles may be barrel re-exports; confirm before refactoring.",
|
|
76
|
+
"dead_in_file": (
|
|
77
|
+
"SCIP may miss dynamic loading and default-export indirection — verify with rdeps/rg before deleting."
|
|
78
|
+
),
|
|
79
|
+
"unreferenced_in_file": (
|
|
80
|
+
"No mentions in the index — may still be used in-file via handlers or dynamic registration."
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _preface_for(key: str) -> str | None:
|
|
86
|
+
return FALSE_POSITIVE_PREFACES.get(key)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class RowBudget:
|
|
91
|
+
"""Shared cap on result rows across analyze sections."""
|
|
92
|
+
|
|
93
|
+
remaining: int
|
|
94
|
+
|
|
95
|
+
def exhausted(self) -> bool:
|
|
96
|
+
return self.remaining <= 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_checks(
|
|
100
|
+
checks: list[Check],
|
|
101
|
+
db,
|
|
102
|
+
limit: int,
|
|
103
|
+
priorities: set[Priority] | None,
|
|
104
|
+
**kwargs: Any,
|
|
105
|
+
) -> list[tuple[str, list[str], str | None]]:
|
|
106
|
+
"""Run checks in priority order (high → low), optionally filtered."""
|
|
107
|
+
selected = [check for check in checks if priorities is None or check.priority in priorities]
|
|
108
|
+
selected.sort(key=lambda check: (_ORDER.index(check.priority), check.key))
|
|
109
|
+
budget: RowBudget = kwargs.pop("budget", None) or RowBudget(remaining=limit)
|
|
110
|
+
sections: list[tuple[str, list[str], str | None]] = []
|
|
111
|
+
for check in selected:
|
|
112
|
+
if budget.exhausted():
|
|
113
|
+
break
|
|
114
|
+
lines = check.run(db, budget.remaining, **kwargs)
|
|
115
|
+
if lines != ["(none)"]:
|
|
116
|
+
lines = lines[: budget.remaining]
|
|
117
|
+
budget.remaining -= len(lines)
|
|
118
|
+
preface = check.false_positive_preface if check.false_positive_preface else _preface_for(check.key)
|
|
119
|
+
sections.append(section(check.labeled_title(), lines, preface=preface))
|
|
120
|
+
return sections
|
|
@@ -168,7 +168,8 @@ def run_all(
|
|
|
168
168
|
symbol_id: int,
|
|
169
169
|
limit: int = DEFAULT_LIMIT,
|
|
170
170
|
priorities=None,
|
|
171
|
-
|
|
171
|
+
budget=None,
|
|
172
|
+
) -> list[tuple[str, list[str], str | None]]:
|
|
172
173
|
checks = [
|
|
173
174
|
Check("consumer_files", Priority.HIGH, "Consumer files (direct)", _bind_symbol(consumer_files, symbol_id)),
|
|
174
175
|
Check("dependencies", Priority.HIGH, "Dependencies (cross-file)", _bind_symbol(dependencies, symbol_id)),
|
|
@@ -181,4 +182,4 @@ def run_all(
|
|
|
181
182
|
),
|
|
182
183
|
Check("def_context", Priority.LOW, "Definition context", _bind_symbol0(def_context, symbol_id)),
|
|
183
184
|
]
|
|
184
|
-
return run_checks(checks, db, limit, priorities)
|
|
185
|
+
return run_checks(checks, db, limit, priorities, budget=budget)
|