scip-cli 2.1.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.1.0 → scip_cli-2.2.1}/.cursor/rules/agent.mdc +33 -10
- {scip_cli-2.1.0 → scip_cli-2.2.1}/PKG-INFO +2 -2
- {scip_cli-2.1.0 → scip_cli-2.2.1}/README.md +1 -1
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/SKILL.md +3 -1
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/__init__.py +1 -1
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/__main__.py +1 -1
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/analyze/common.py +29 -6
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/analyze/file.py +55 -22
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/analyze/graph.py +4 -1
- scip_cli-2.2.1/scip_cli/analyze/live.py +168 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/analyze/project.py +77 -39
- scip_cli-2.2.1/scip_cli/analyze/sections.py +120 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/analyze/symbol.py +3 -2
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/analyze.py +32 -10
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/refs.py +4 -2
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/output.py +17 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/queries.py +21 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/symbols.py +26 -0
- scip_cli-2.2.1/scripts/publish.sh +96 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/analyze_db.py +63 -0
- scip_cli-2.2.1/tests/fixture_catalog.py +44 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/domain/i18n/en.ts +3 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/domain/i18n/index.ts +3 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/domain/i18n/useLocale.ts +5 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/domain/labels/index.ts +3 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/events/streamTypes.ts +7 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/gateway/index.ts +3 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/hooks/useHookA.ts +7 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/hooks/useHookB.ts +7 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/hooks/useItems.ts +5 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/integrations/inferenceClient.ts +7 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/loaders/useInference.ts +5 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/pages/panelPage.ts +4 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/rules/applyRule.ts +5 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/types/a.ts +3 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/types/b.ts +3 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/ui/Button.ts +7 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/ui/LazyPanel.ts +3 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/ui/buttonConsumer.ts +5 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/ui/menuModule.ts +1 -0
- scip_cli-2.2.1/tests/fixtures/sample-project/src/widgets/OrphanWidget.ts +3 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_analyze.py +65 -10
- scip_cli-2.2.1/tests/test_analyze_graph.py +66 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_e2e.py +2 -2
- scip_cli-2.2.1/tests/test_e2e_analyze_patterns.py +168 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_pure_functions.py +9 -1
- scip_cli-2.1.0/scip_cli/analyze/sections.py +0 -77
- 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.1}/.gitignore +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/.pre-commit-config.yaml +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/LICENSE +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/pyproject.toml +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/analyze/__init__.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/analyze/targets.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/cache.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/cli_args.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/__init__.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/code.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/members.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/rdeps.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/reindex.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/search.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/skill.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/commands/symbols.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/config.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/debug.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/discover.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/indexing.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/merge.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/paths.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/project.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/scip_tool.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/scope.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/session.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/source.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/sql.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scip_cli/targets.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scripts/build.sh +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/scripts/test.sh +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/__init__.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/conftest.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/e2e_harness.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/package.json +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/app/handler.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/config.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/consumer.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/dead.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/helper.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/index.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/user.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/src/widget.ts +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/fixtures/sample-project/tsconfig.json +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/perf_util.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_analyze_perf.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_cache.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_composability.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_config.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_discover.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_e2e_perf.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_index_prune.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_indexer_env.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_merge.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_multi_symbol.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_qualified_symbols.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_reindex.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_scip_tool.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_scope.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/tests/test_targets.py +0 -0
- {scip_cli-2.1.0 → scip_cli-2.2.1}/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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scip-cli
|
|
3
|
-
Version: 2.1
|
|
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",
|
|
@@ -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"))
|
|
@@ -104,7 +127,7 @@ def cycle_path_noise(cycle_line: str, *, include_tests: bool = False) -> bool:
|
|
|
104
127
|
return any(is_test_path(part.strip()) for part in parts if part.strip())
|
|
105
128
|
|
|
106
129
|
|
|
107
|
-
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]:
|
|
108
131
|
if not lines:
|
|
109
|
-
return title, ["(none)"]
|
|
110
|
-
return title, lines
|
|
132
|
+
return title, ["(none)"], None
|
|
133
|
+
return title, lines, preface
|
|
@@ -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, file_has_scip_importers, has_same_file_reference_usage
|
|
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,18 +106,22 @@ 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
|
"""
|
|
118
|
-
SELECT gs.symbol, der.start_line, der.end_line
|
|
124
|
+
SELECT gs.symbol, der.start_line, der.end_line, def_d.id
|
|
119
125
|
FROM global_symbols gs
|
|
120
126
|
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
121
127
|
JOIN documents def_d ON der.document_id = def_d.id
|
|
@@ -135,18 +141,24 @@ 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, def_doc_id in rows:
|
|
146
|
+
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
147
|
+
continue
|
|
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):
|
|
151
|
+
continue
|
|
152
|
+
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
153
|
+
return lines
|
|
143
154
|
|
|
144
155
|
|
|
145
156
|
def dead_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
157
|
+
live = LiveIndex(db)
|
|
146
158
|
rows = fetch_all(
|
|
147
159
|
db,
|
|
148
160
|
"""
|
|
149
|
-
SELECT gs.symbol, der.start_line, der.end_line
|
|
161
|
+
SELECT gs.id, gs.symbol, der.start_line, der.end_line, def_d.id
|
|
150
162
|
FROM global_symbols gs
|
|
151
163
|
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
152
164
|
JOIN documents def_d ON der.document_id = def_d.id
|
|
@@ -164,11 +176,16 @@ def dead_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str
|
|
|
164
176
|
""",
|
|
165
177
|
(relative_path, limit),
|
|
166
178
|
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
179
|
+
lines = []
|
|
180
|
+
for sym_id, symbol, start, end, def_doc_id in rows:
|
|
181
|
+
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
182
|
+
continue
|
|
183
|
+
if has_same_file_reference_usage(db, sym_id, def_doc_id):
|
|
184
|
+
continue
|
|
185
|
+
if live.dead_export_noise(symbol, def_doc_id):
|
|
186
|
+
continue
|
|
187
|
+
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
188
|
+
return lines
|
|
172
189
|
|
|
173
190
|
|
|
174
191
|
def imports_summary(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
@@ -240,6 +257,16 @@ def coupling_for(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str
|
|
|
240
257
|
return [f"{other} shared={shared}" for other, shared in rows]
|
|
241
258
|
|
|
242
259
|
|
|
260
|
+
def count_file_importers(db, relative_path: str) -> int:
|
|
261
|
+
from ..queries import get_file_symbols, get_importer_paths
|
|
262
|
+
|
|
263
|
+
symbols = get_file_symbols(db, relative_path)
|
|
264
|
+
if not symbols:
|
|
265
|
+
return 0
|
|
266
|
+
symbol_ids = [row[0] for row in symbols]
|
|
267
|
+
return len(get_importer_paths(db, symbol_ids, relative_path))
|
|
268
|
+
|
|
269
|
+
|
|
243
270
|
def top_symbol_pressure(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
244
271
|
"""Pressure metrics for the most-consumed exports in a file."""
|
|
245
272
|
cap = min(5, limit)
|
|
@@ -321,8 +348,10 @@ def _run_file_checks(
|
|
|
321
348
|
db,
|
|
322
349
|
limit: int,
|
|
323
350
|
priorities,
|
|
324
|
-
|
|
325
|
-
|
|
351
|
+
*,
|
|
352
|
+
budget=None,
|
|
353
|
+
) -> list[tuple[str, list[str], str | None]]:
|
|
354
|
+
return run_checks(checks, db, limit, priorities, budget=budget)
|
|
326
355
|
|
|
327
356
|
|
|
328
357
|
def run_all(
|
|
@@ -330,12 +359,14 @@ def run_all(
|
|
|
330
359
|
relative_path: str,
|
|
331
360
|
limit: int = DEFAULT_LIMIT,
|
|
332
361
|
priorities=None,
|
|
333
|
-
|
|
362
|
+
budget=None,
|
|
363
|
+
) -> list[tuple[str, list[str], str | None]]:
|
|
334
364
|
return _run_file_checks(
|
|
335
365
|
_file_checks(relative_path, include_top_symbols=True),
|
|
336
366
|
db,
|
|
337
367
|
limit,
|
|
338
368
|
priorities,
|
|
369
|
+
budget=budget,
|
|
339
370
|
)
|
|
340
371
|
|
|
341
372
|
|
|
@@ -344,11 +375,13 @@ def run_all_sections_only(
|
|
|
344
375
|
relative_path: str,
|
|
345
376
|
limit: int = DEFAULT_LIMIT,
|
|
346
377
|
priorities=None,
|
|
347
|
-
|
|
378
|
+
budget=None,
|
|
379
|
+
) -> list[tuple[str, list[str], str | None]]:
|
|
348
380
|
"""Per-file sections without top-symbols (directory batch)."""
|
|
349
381
|
return _run_file_checks(
|
|
350
382
|
_file_checks(relative_path, include_top_symbols=False),
|
|
351
383
|
db,
|
|
352
384
|
limit,
|
|
353
385
|
priorities,
|
|
386
|
+
budget=budget,
|
|
354
387
|
)
|
|
@@ -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,168 @@
|
|
|
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.module_importers: dict[int, int] = {}
|
|
68
|
+
self._build(db)
|
|
69
|
+
|
|
70
|
+
def _build(self, db) -> None:
|
|
71
|
+
for doc_id, importer_count in _fetch_all(
|
|
72
|
+
db,
|
|
73
|
+
"""
|
|
74
|
+
SELECT der.document_id, COUNT(DISTINCT c.document_id)
|
|
75
|
+
FROM global_symbols gs
|
|
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
|
|
79
|
+
WHERE gs.symbol LIKE '%/' AND gs.symbol NOT LIKE '%().'
|
|
80
|
+
AND c.document_id != der.document_id
|
|
81
|
+
GROUP BY der.document_id
|
|
82
|
+
""",
|
|
83
|
+
):
|
|
84
|
+
self.live_module_docs.add(doc_id)
|
|
85
|
+
self.module_importers[doc_id] = importer_count
|
|
86
|
+
|
|
87
|
+
for sym_id, symbol in _fetch_all(
|
|
88
|
+
db,
|
|
89
|
+
"""
|
|
90
|
+
SELECT id, symbol FROM global_symbols
|
|
91
|
+
WHERE symbol LIKE '%0:' AND symbol NOT LIKE '%().'
|
|
92
|
+
""",
|
|
93
|
+
):
|
|
94
|
+
base = export_alias_base(symbol)
|
|
95
|
+
if not base:
|
|
96
|
+
continue
|
|
97
|
+
def_doc = _def_doc_id_from_symbol(db, symbol)
|
|
98
|
+
if def_doc is None:
|
|
99
|
+
continue
|
|
100
|
+
if _fetch_one(db, _EXTERNAL_MENTION, (sym_id, def_doc)):
|
|
101
|
+
self.live_alias_bases.add(base)
|
|
102
|
+
|
|
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)."""
|
|
105
|
+
if is_module_symbol(symbol):
|
|
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}"
|
|
110
|
+
base = export_value_base(symbol)
|
|
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
|
|
119
|
+
|
|
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:
|
|
125
|
+
"""Export used only in-file but live via module or export alias elsewhere."""
|
|
126
|
+
base = export_value_base(symbol)
|
|
127
|
+
if not base:
|
|
128
|
+
return False
|
|
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
|
|
132
|
+
|
|
133
|
+
def stale_type_live_noise(self, symbol: str, def_doc_id: int) -> bool:
|
|
134
|
+
"""Class/type looks unused but file is live via default export or module import."""
|
|
135
|
+
base = export_value_base(symbol)
|
|
136
|
+
if not base:
|
|
137
|
+
return False
|
|
138
|
+
if base in self.live_alias_bases:
|
|
139
|
+
return True
|
|
140
|
+
return def_doc_id in self.live_module_docs
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def has_same_file_reference_usage(db, symbol_id: int, def_doc_id: int) -> bool:
|
|
144
|
+
"""Type or value referenced in the same file (extends, signatures, handler registration)."""
|
|
145
|
+
row = _fetch_one(
|
|
146
|
+
db,
|
|
147
|
+
"""
|
|
148
|
+
SELECT 1 FROM mentions m
|
|
149
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
150
|
+
WHERE m.symbol_id = ? AND m.role = 0 AND c.document_id = ?
|
|
151
|
+
LIMIT 1
|
|
152
|
+
""",
|
|
153
|
+
(symbol_id, def_doc_id),
|
|
154
|
+
)
|
|
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))
|