roam-code 8.0.1__tar.gz → 8.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {roam_code-8.0.1 → roam_code-8.1.0}/PKG-INFO +1 -1
- {roam_code-8.0.1 → roam_code-8.1.0}/pyproject.toml +4 -1
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_complexity.py +20 -9
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_dead.py +28 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_health.py +10 -2
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/python_lang.py +72 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam_code.egg-info/PKG-INFO +1 -1
- {roam_code-8.0.1 → roam_code-8.1.0}/LICENSE +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/README.md +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/setup.cfg +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/__main__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/bridges/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/bridges/base.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/bridges/bridge_protobuf.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/bridges/bridge_salesforce.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/bridges/registry.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/cli.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/changed_files.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_affected_tests.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_alerts.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_breaking.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_bus_factor.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_clusters.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_context.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_conventions.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_coupling.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_coverage_gaps.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_debt.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_deps.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_describe.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_diagnose.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_diff.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_digest.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_doc_staleness.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_entry_points.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_fan.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_file.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_fitness.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_fn_coupling.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_grep.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_impact.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_index.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_init.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_layers.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_map.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_module.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_owner.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_patterns.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_pr_risk.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_preflight.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_report.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_risk.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_safe_delete.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_safe_zones.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_search.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_sketch.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_snapshot.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_split.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_symbol.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_testmap.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_tour.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_trace.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_trend.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_understand.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_uses.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_visualize.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_weather.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_why.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_ws.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/cmd_xlang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/context_helpers.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/gate_presets.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/graph_helpers.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/metrics_history.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/commands/resolve.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/db/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/db/connection.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/db/queries.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/db/schema.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/graph/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/graph/anomaly.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/graph/builder.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/graph/clusters.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/graph/cycles.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/graph/layers.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/graph/pagerank.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/graph/pathfinding.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/complexity.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/discovery.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/file_roles.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/git_stats.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/incremental.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/indexer.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/parser.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/relations.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/symbols.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/index/test_conventions.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/apex_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/aura_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/base.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/c_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/foxpro_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/generic_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/go_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/java_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/javascript_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/php_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/registry.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/rust_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/sfxml_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/typescript_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/languages/visualforce_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/mcp_server.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/output/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/output/formatter.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/output/sarif.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/workspace/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/workspace/aggregator.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/workspace/api_scanner.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/workspace/config.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam/workspace/db.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam_code.egg-info/SOURCES.txt +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam_code.egg-info/dependency_links.txt +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam_code.egg-info/entry_points.txt +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam_code.egg-info/requires.txt +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/src/roam_code.egg-info/top_level.txt +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_anomaly.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_basic.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_bridges.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_commands_architecture.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_commands_exploration.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_commands_health.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_commands_refactoring.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_commands_workflow.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_comprehensive.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_dead_aging.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_file_roles.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_fixes.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_formatters.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_foxpro.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_gate_presets.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_json_contracts.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_languages.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_performance.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_pr_risk_author.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_resolve.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_salesforce.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_smoke.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_test_conventions.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_v6_features.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_v71_features.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_v7_features.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_visualize.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.0}/tests/test_workspace.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "roam-code"
|
|
7
|
-
version = "8.0
|
|
7
|
+
version = "8.1.0"
|
|
8
8
|
description = "Instant codebase comprehension for AI coding agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -55,6 +55,9 @@ where = ["src"]
|
|
|
55
55
|
[tool.pytest.ini_options]
|
|
56
56
|
testpaths = ["tests"]
|
|
57
57
|
addopts = "-q --tb=short"
|
|
58
|
+
markers = [
|
|
59
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
60
|
+
]
|
|
58
61
|
|
|
59
62
|
[tool.ruff]
|
|
60
63
|
target-version = "py39"
|
|
@@ -5,6 +5,8 @@ functions/methods by cognitive complexity to identify the hardest-to-
|
|
|
5
5
|
understand code in the project.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
8
10
|
import click
|
|
9
11
|
|
|
10
12
|
from roam.db.connection import open_db
|
|
@@ -12,6 +14,15 @@ from roam.commands.resolve import ensure_index
|
|
|
12
14
|
from roam.output.formatter import loc, abbrev_kind, to_json, json_envelope
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
def _safe_metric(row, key, default=0.0):
|
|
18
|
+
"""Safely access a metric column that may not exist in older DBs."""
|
|
19
|
+
try:
|
|
20
|
+
v = row[key]
|
|
21
|
+
return v if v is not None else default
|
|
22
|
+
except (KeyError, IndexError):
|
|
23
|
+
return default
|
|
24
|
+
|
|
25
|
+
|
|
15
26
|
def _severity(score: float) -> str:
|
|
16
27
|
"""Map cognitive complexity score to a severity label."""
|
|
17
28
|
if score >= 25:
|
|
@@ -144,11 +155,11 @@ def complexity(ctx, target, limit, threshold, by_file, bumpy_road):
|
|
|
144
155
|
"return_count": r["return_count"],
|
|
145
156
|
"bool_op_count": r["bool_op_count"],
|
|
146
157
|
"callback_depth": r["callback_depth"],
|
|
147
|
-
"cyclomatic_density": r
|
|
148
|
-
"halstead_volume": r
|
|
149
|
-
"halstead_difficulty": r
|
|
150
|
-
"halstead_effort": r
|
|
151
|
-
"halstead_bugs": r
|
|
158
|
+
"cyclomatic_density": _safe_metric(r, "cyclomatic_density"),
|
|
159
|
+
"halstead_volume": _safe_metric(r, "halstead_volume"),
|
|
160
|
+
"halstead_difficulty": _safe_metric(r, "halstead_difficulty"),
|
|
161
|
+
"halstead_effort": _safe_metric(r, "halstead_effort"),
|
|
162
|
+
"halstead_bugs": _safe_metric(r, "halstead_bugs"),
|
|
152
163
|
"severity": _severity(r["cognitive_complexity"]),
|
|
153
164
|
}
|
|
154
165
|
for r in rows
|
|
@@ -181,11 +192,11 @@ def complexity(ctx, target, limit, threshold, by_file, bumpy_road):
|
|
|
181
192
|
factors.append(f"params={r['param_count']}")
|
|
182
193
|
if r["return_count"] >= 4:
|
|
183
194
|
factors.append(f"ret={r['return_count']}")
|
|
184
|
-
cd = r
|
|
185
|
-
if cd
|
|
195
|
+
cd = _safe_metric(r, "cyclomatic_density")
|
|
196
|
+
if cd > 0.15:
|
|
186
197
|
factors.append(f"density={cd:.2f}")
|
|
187
|
-
hv = r
|
|
188
|
-
if hv
|
|
198
|
+
hv = _safe_metric(r, "halstead_volume")
|
|
199
|
+
if hv > 500:
|
|
189
200
|
factors.append(f"H.vol={hv:.0f}")
|
|
190
201
|
|
|
191
202
|
factor_str = f" ({', '.join(factors)})" if factors else ""
|
|
@@ -40,6 +40,20 @@ _API_PREFIXES = ("get", "use", "create", "validate", "fetch", "update",
|
|
|
40
40
|
"delete", "find", "check", "make", "build", "parse")
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
_ABC_METHOD_NAMES = frozenset({
|
|
44
|
+
"language_name", "file_extensions", "extract_symbols", "extract_references",
|
|
45
|
+
"get_docstring", "get_signature", "node_text",
|
|
46
|
+
"detect", "supported_bridges",
|
|
47
|
+
"resolve_cross_language", "get_bridge_edges",
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_test_path(file_path):
|
|
52
|
+
"""Check if a file is a test file (discovered by pytest, not imported)."""
|
|
53
|
+
base = os.path.basename(file_path).lower()
|
|
54
|
+
return base.startswith("test_") or base.endswith("_test.py")
|
|
55
|
+
|
|
56
|
+
|
|
43
57
|
def _dead_action(r, file_imported):
|
|
44
58
|
"""Compute actionable verdict and confidence % for a dead symbol.
|
|
45
59
|
|
|
@@ -62,6 +76,18 @@ def _dead_action(r, file_imported):
|
|
|
62
76
|
except (KeyError, IndexError):
|
|
63
77
|
kind = ""
|
|
64
78
|
|
|
79
|
+
# Test file symbols — discovered by pytest, never imported directly
|
|
80
|
+
if _is_test_path(r["file_path"]):
|
|
81
|
+
return "INTENTIONAL", 10
|
|
82
|
+
|
|
83
|
+
# CLI command functions — loaded dynamically via LazyGroup/importlib
|
|
84
|
+
if base.startswith("cmd_") and kind == "function":
|
|
85
|
+
return "INTENTIONAL", 20
|
|
86
|
+
|
|
87
|
+
# ABC method overrides — called polymorphically, not by direct import
|
|
88
|
+
if kind == "method" and name in _ABC_METHOD_NAMES:
|
|
89
|
+
return "INTENTIONAL", 10
|
|
90
|
+
|
|
65
91
|
# Entry point / lifecycle hooks (check original case for camelCase hooks)
|
|
66
92
|
if name in _ENTRY_NAMES or name_lower in _ENTRY_NAMES:
|
|
67
93
|
return "INTENTIONAL", 60
|
|
@@ -251,6 +277,8 @@ def _analyze_dead(conn):
|
|
|
251
277
|
Returns (high, low, imported_files) where high/low are lists of Row objects.
|
|
252
278
|
"""
|
|
253
279
|
rows = conn.execute(UNREFERENCED_EXPORTS).fetchall()
|
|
280
|
+
# Exclude test files — their symbols are discovered by pytest, not imported
|
|
281
|
+
rows = [r for r in rows if not _is_test_path(r["file_path"])]
|
|
254
282
|
if not rows:
|
|
255
283
|
return [], [], set()
|
|
256
284
|
|
|
@@ -52,13 +52,21 @@ _FRAMEWORK_NAMES = frozenset({
|
|
|
52
52
|
_UTILITY_PATH_PATTERNS = (
|
|
53
53
|
"composables/", "utils/", "services/", "lib/", "helpers/",
|
|
54
54
|
"shared/", "config/", "core/", "hooks/", "stores/",
|
|
55
|
+
"output/", "db/", "common/", "internal/", "infra/",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_UTILITY_FILE_PATTERNS = (
|
|
59
|
+
"resolve.py", "helpers.py", "common.py", "base.py",
|
|
55
60
|
)
|
|
56
61
|
|
|
57
62
|
|
|
58
63
|
def _is_utility_path(file_path):
|
|
59
|
-
"""Check if a file is in a utility/infrastructure directory."""
|
|
64
|
+
"""Check if a file is in a utility/infrastructure directory or is a known utility file."""
|
|
60
65
|
p = file_path.replace("\\", "/").lower()
|
|
61
|
-
|
|
66
|
+
if any(pat in p for pat in _UTILITY_PATH_PATTERNS):
|
|
67
|
+
return True
|
|
68
|
+
basename = p.rsplit("/", 1)[-1] if "/" in p else p
|
|
69
|
+
return basename in _UTILITY_FILE_PATTERNS
|
|
62
70
|
|
|
63
71
|
|
|
64
72
|
def _percentile(sorted_values, pct):
|
|
@@ -333,6 +333,9 @@ class PythonExtractor(LanguageExtractor):
|
|
|
333
333
|
self._extract_from_import(child, source, refs, scope_name)
|
|
334
334
|
elif child.type == "call":
|
|
335
335
|
self._extract_call(child, source, refs, scope_name)
|
|
336
|
+
elif child.type == "decorated_definition":
|
|
337
|
+
self._extract_decorator_refs(child, source, refs, scope_name)
|
|
338
|
+
self._walk_refs(child, source, file_path, refs, scope_name)
|
|
336
339
|
else:
|
|
337
340
|
# Recurse, updating scope for classes/functions
|
|
338
341
|
new_scope = scope_name
|
|
@@ -341,8 +344,77 @@ class PythonExtractor(LanguageExtractor):
|
|
|
341
344
|
if n:
|
|
342
345
|
fname = self.node_text(n, source)
|
|
343
346
|
new_scope = f"{scope_name}.{fname}" if scope_name else fname
|
|
347
|
+
# Extract type annotation refs from function parameters and return
|
|
348
|
+
if child.type == "function_definition":
|
|
349
|
+
self._extract_type_refs(child, source, refs, new_scope)
|
|
344
350
|
self._walk_refs(child, source, file_path, refs, new_scope)
|
|
345
351
|
|
|
352
|
+
def _extract_decorator_refs(self, decorated_node, source, refs, scope_name):
|
|
353
|
+
"""Extract references from decorators (e.g., @cache, @app.route)."""
|
|
354
|
+
for child in decorated_node.children:
|
|
355
|
+
if child.type == "decorator":
|
|
356
|
+
# The decorator content is after the '@'
|
|
357
|
+
for sub in child.children:
|
|
358
|
+
if sub.type == "identifier":
|
|
359
|
+
name = self.node_text(sub, source)
|
|
360
|
+
refs.append(self._make_reference(
|
|
361
|
+
target_name=name,
|
|
362
|
+
kind="call",
|
|
363
|
+
line=sub.start_point[0] + 1,
|
|
364
|
+
source_name=scope_name,
|
|
365
|
+
))
|
|
366
|
+
elif sub.type == "attribute":
|
|
367
|
+
name = self.node_text(sub, source)
|
|
368
|
+
refs.append(self._make_reference(
|
|
369
|
+
target_name=name,
|
|
370
|
+
kind="call",
|
|
371
|
+
line=sub.start_point[0] + 1,
|
|
372
|
+
source_name=scope_name,
|
|
373
|
+
))
|
|
374
|
+
elif sub.type == "call":
|
|
375
|
+
self._extract_call(sub, source, refs, scope_name)
|
|
376
|
+
|
|
377
|
+
def _extract_type_refs(self, func_node, source, refs, scope_name):
|
|
378
|
+
"""Extract references from type annotations in function signatures."""
|
|
379
|
+
# Parameter type annotations
|
|
380
|
+
params = func_node.child_by_field_name("parameters")
|
|
381
|
+
if params:
|
|
382
|
+
for param in params.children:
|
|
383
|
+
type_node = param.child_by_field_name("type")
|
|
384
|
+
if type_node:
|
|
385
|
+
self._walk_type_node(type_node, source, refs, scope_name)
|
|
386
|
+
|
|
387
|
+
# Return type annotation
|
|
388
|
+
ret = func_node.child_by_field_name("return_type")
|
|
389
|
+
if ret:
|
|
390
|
+
self._walk_type_node(ret, source, refs, scope_name)
|
|
391
|
+
|
|
392
|
+
def _walk_type_node(self, node, source, refs, scope_name):
|
|
393
|
+
"""Walk a type annotation node and extract type references."""
|
|
394
|
+
if node.type == "identifier":
|
|
395
|
+
name = self.node_text(node, source)
|
|
396
|
+
# Skip builtins that don't create real references
|
|
397
|
+
if name not in ("int", "str", "float", "bool", "bytes", "None",
|
|
398
|
+
"list", "dict", "set", "tuple", "type", "object"):
|
|
399
|
+
refs.append(self._make_reference(
|
|
400
|
+
target_name=name,
|
|
401
|
+
kind="type_ref",
|
|
402
|
+
line=node.start_point[0] + 1,
|
|
403
|
+
source_name=scope_name,
|
|
404
|
+
))
|
|
405
|
+
elif node.type == "attribute":
|
|
406
|
+
name = self.node_text(node, source)
|
|
407
|
+
refs.append(self._make_reference(
|
|
408
|
+
target_name=name,
|
|
409
|
+
kind="type_ref",
|
|
410
|
+
line=node.start_point[0] + 1,
|
|
411
|
+
source_name=scope_name,
|
|
412
|
+
))
|
|
413
|
+
else:
|
|
414
|
+
# Recurse into generic types like List[Item], Optional[str], etc.
|
|
415
|
+
for child in node.children:
|
|
416
|
+
self._walk_type_node(child, source, refs, scope_name)
|
|
417
|
+
|
|
346
418
|
def _extract_import(self, node, source, refs, scope_name):
|
|
347
419
|
# import x, import x.y, import x as y
|
|
348
420
|
for child in node.children:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|