codmap 0.0.9__tar.gz → 0.0.11__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.
- {codmap-0.0.9/codmap.egg-info → codmap-0.0.11}/PKG-INFO +16 -3
- {codmap-0.0.9 → codmap-0.0.11}/README.md +15 -2
- {codmap-0.0.9 → codmap-0.0.11}/codemap/cli.py +34 -2
- {codmap-0.0.9 → codmap-0.0.11}/codemap/diagnostics.py +76 -7
- {codmap-0.0.9 → codmap-0.0.11}/codemap/extract/behavior.py +95 -1
- {codmap-0.0.9 → codmap-0.0.11}/codemap/extract/griffe_extractor.py +47 -4
- {codmap-0.0.9 → codmap-0.0.11}/codemap/extract/roots.py +30 -5
- codmap-0.0.11/codemap/extract/union.py +144 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/incremental.py +43 -7
- {codmap-0.0.9 → codmap-0.0.11}/codemap/provenance.py +34 -5
- {codmap-0.0.9 → codmap-0.0.11}/codemap/query.py +18 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/audit.py +16 -0
- {codmap-0.0.9 → codmap-0.0.11/codmap.egg-info}/PKG-INFO +16 -3
- {codmap-0.0.9 → codmap-0.0.11}/codmap.egg-info/SOURCES.txt +4 -0
- {codmap-0.0.9 → codmap-0.0.11}/pyproject.toml +1 -1
- codmap-0.0.11/tests/test_r1c43_incremental_splice.py +220 -0
- codmap-0.0.11/tests/test_r1c45_repeat_union.py +236 -0
- codmap-0.0.11/tests/test_r1c46_shadowed_definitions.py +142 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c9_incremental.py +32 -5
- {codmap-0.0.9 → codmap-0.0.11}/LICENSE +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/__init__.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/apidiff.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/arch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/freshness.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/integrations/base.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/model.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/scope.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/apidiff.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/check.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/impact.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/limits.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/mcp_server.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/pack.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/rag.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/review.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/scip.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/server.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/session.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/serve/vault.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/store.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/tomlio.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codemap/watch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/setup.cfg +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_epistemic.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_integrations.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m15_review.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m1_query.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m2_views.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_m9_family.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_scip_export.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_scope.py +0 -0
- {codmap-0.0.9 → codmap-0.0.11}/tests/test_subsystems.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codmap
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.11
|
|
4
4
|
Summary: Static analyzer that turns a package's source into a queryable code graph.
|
|
5
5
|
Author-email: kogriv <kogriv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -44,7 +44,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
|
|
|
44
44
|
|
|
45
45
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
46
46
|
|
|
47
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
47
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **765 tests with no failures on
|
|
48
48
|
Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
|
|
49
49
|
wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
|
|
50
50
|
as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
|
|
@@ -167,6 +167,9 @@ codemap build ./yourpkg -o graph.json
|
|
|
167
167
|
# repo-scoped: add consumers (tests/examples) + docs for blast-radius/impact
|
|
168
168
|
codemap build ./yourpkg --deep --mode full --consumer ./tests --docs ./docs -o graph.json
|
|
169
169
|
|
|
170
|
+
# deep tier: union N independent samples (one build misses a real edge ~1 time in 4)
|
|
171
|
+
codemap build ./yourpkg --deep --repeat 3 -o graph.json
|
|
172
|
+
|
|
170
173
|
# ask about a symbol (JSON by default; --format text for humans)
|
|
171
174
|
codemap query analyze_zones --graph graph.json
|
|
172
175
|
|
|
@@ -262,7 +265,17 @@ build says so in its own diagnostics, and `codemap diff` says so when both sides
|
|
|
262
265
|
delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
|
|
263
266
|
that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
|
|
264
267
|
|
|
265
|
-
**
|
|
268
|
+
**How many times to build.** A consumer measured 175 full deep builds of one tree and found a real
|
|
269
|
+
edge present in **75 %** of them; eight builds here agreed (5 of 8, the same edge, and nothing else
|
|
270
|
+
unstable among 13 675). So one build misses such an edge about 1 time in 4, two builds 1 in 16, three
|
|
271
|
+
1 in 64. `codemap build --deep --repeat N` builds N samples — each in a fresh interpreter, the
|
|
272
|
+
regime the share was measured in; in-process repeats come in correlated streaks — and unions them: an edge seen in fewer
|
|
273
|
+
than N runs carries `extras.seen`, `provenance.samples` records N and how many edges varied, and the
|
|
274
|
+
note on the graph says both. Refused out loud on the fast tier (byte-stable already) and together with
|
|
275
|
+
`--incremental` (a spliced graph cannot be resampled).
|
|
276
|
+
|
|
277
|
+
**Measurement:** [gaps/deep_tier_nondeterminism_2026-09-02.md](gaps/deep_tier_nondeterminism_2026-09-02.md),
|
|
278
|
+
[gaps/deep_tier_union_by_repeat_2026-09-04.md](gaps/deep_tier_union_by_repeat_2026-09-04.md).
|
|
266
279
|
|
|
267
280
|
## Dogfooding
|
|
268
281
|
|
|
@@ -8,7 +8,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
|
|
|
8
8
|
|
|
9
9
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
10
10
|
|
|
11
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
11
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **765 tests with no failures on
|
|
12
12
|
Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
|
|
13
13
|
wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
|
|
14
14
|
as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
|
|
@@ -131,6 +131,9 @@ codemap build ./yourpkg -o graph.json
|
|
|
131
131
|
# repo-scoped: add consumers (tests/examples) + docs for blast-radius/impact
|
|
132
132
|
codemap build ./yourpkg --deep --mode full --consumer ./tests --docs ./docs -o graph.json
|
|
133
133
|
|
|
134
|
+
# deep tier: union N independent samples (one build misses a real edge ~1 time in 4)
|
|
135
|
+
codemap build ./yourpkg --deep --repeat 3 -o graph.json
|
|
136
|
+
|
|
134
137
|
# ask about a symbol (JSON by default; --format text for humans)
|
|
135
138
|
codemap query analyze_zones --graph graph.json
|
|
136
139
|
|
|
@@ -226,7 +229,17 @@ build says so in its own diagnostics, and `codemap diff` says so when both sides
|
|
|
226
229
|
delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
|
|
227
230
|
that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
|
|
228
231
|
|
|
229
|
-
**
|
|
232
|
+
**How many times to build.** A consumer measured 175 full deep builds of one tree and found a real
|
|
233
|
+
edge present in **75 %** of them; eight builds here agreed (5 of 8, the same edge, and nothing else
|
|
234
|
+
unstable among 13 675). So one build misses such an edge about 1 time in 4, two builds 1 in 16, three
|
|
235
|
+
1 in 64. `codemap build --deep --repeat N` builds N samples — each in a fresh interpreter, the
|
|
236
|
+
regime the share was measured in; in-process repeats come in correlated streaks — and unions them: an edge seen in fewer
|
|
237
|
+
than N runs carries `extras.seen`, `provenance.samples` records N and how many edges varied, and the
|
|
238
|
+
note on the graph says both. Refused out loud on the fast tier (byte-stable already) and together with
|
|
239
|
+
`--incremental` (a spliced graph cannot be resampled).
|
|
240
|
+
|
|
241
|
+
**Measurement:** [gaps/deep_tier_nondeterminism_2026-09-02.md](gaps/deep_tier_nondeterminism_2026-09-02.md),
|
|
242
|
+
[gaps/deep_tier_union_by_repeat_2026-09-04.md](gaps/deep_tier_union_by_repeat_2026-09-04.md).
|
|
230
243
|
|
|
231
244
|
## Dogfooding
|
|
232
245
|
|
|
@@ -83,6 +83,24 @@ def _graph_from(args):
|
|
|
83
83
|
|
|
84
84
|
def _cmd_build(args) -> int:
|
|
85
85
|
incr_info = None
|
|
86
|
+
repeat = getattr(args, "repeat", None)
|
|
87
|
+
repeat = 1 if repeat is None else repeat
|
|
88
|
+
# R1-C45 / D7–D8: refused out loud, never swallowed. The fast tier is byte-stable and
|
|
89
|
+
# CI pins it, so N runs there buy nothing and cost N minutes; and an incremental graph's
|
|
90
|
+
# spliced part is an earlier build's sample by definition (R1-C43), so resampling only
|
|
91
|
+
# the affected modules would leave `seen` relative to different N on neighbouring edges.
|
|
92
|
+
if repeat < 1:
|
|
93
|
+
print("error: --repeat must be >= 1", file=sys.stderr)
|
|
94
|
+
return 2
|
|
95
|
+
if repeat > 1 and not args.deep:
|
|
96
|
+
print(f"error: --repeat {repeat} needs --deep — the fast tier is byte-stable, so "
|
|
97
|
+
"repeating it unions identical samples", file=sys.stderr)
|
|
98
|
+
return 2
|
|
99
|
+
if repeat > 1 and getattr(args, "incremental", False):
|
|
100
|
+
print(f"error: --repeat {repeat} cannot be combined with --incremental — the spliced "
|
|
101
|
+
"part of an incremental graph is an earlier build's sample and cannot be "
|
|
102
|
+
"resampled; run a full build", file=sys.stderr)
|
|
103
|
+
return 2
|
|
86
104
|
if args.consumer or args.docs:
|
|
87
105
|
graph = extract_repo(
|
|
88
106
|
args.path,
|
|
@@ -90,11 +108,12 @@ def _cmd_build(args) -> int:
|
|
|
90
108
|
docs=tuple(args.docs or ()),
|
|
91
109
|
mode=args.mode,
|
|
92
110
|
deep=args.deep,
|
|
111
|
+
repeat=repeat,
|
|
93
112
|
)
|
|
94
113
|
elif getattr(args, "incremental", False) and args.out:
|
|
95
114
|
graph, incr_info = _incremental_build(args)
|
|
96
115
|
else:
|
|
97
|
-
graph = extract(args.path, deep=args.deep)
|
|
116
|
+
graph = extract(args.path, deep=args.deep, repeat=repeat)
|
|
98
117
|
# R1-C25: stamp the input's identity into the graph itself. The scope manifest is
|
|
99
118
|
# resolved here once and reused for the sidecar below — the graph gets the part that
|
|
100
119
|
# must travel with it (scope_id, source commit), the sidecar keeps the rebuild recipe.
|
|
@@ -104,7 +123,14 @@ def _cmd_build(args) -> int:
|
|
|
104
123
|
# roots come from extract_repo in this same call; never inherited from a graph
|
|
105
124
|
# loaded off disk, which is how a stale scope would sneak into a fresh build.
|
|
106
125
|
roots=graph.provenance.get("roots") if (args.consumer or args.docs) else None,
|
|
107
|
-
inputs=_inputs_with_membership(graph, scope, args)
|
|
126
|
+
inputs=_inputs_with_membership(graph, scope, args),
|
|
127
|
+
# R1-C43: this call *overwrites* whatever update_graph stamped, so the flag has
|
|
128
|
+
# to be re-derived here or it is lost. `unchanged` counts as carried over too —
|
|
129
|
+
# nothing in that graph was recomputed by this build at all.
|
|
130
|
+
incremental=(incr_info or {}).get("mode") in ("incremental", "unchanged"),
|
|
131
|
+
# R1-C45: same trap — the extractor stamped the sample count; carry it, or a
|
|
132
|
+
# `--repeat 3` graph leaves here saying `runs: 1`.
|
|
133
|
+
samples=graph.provenance.get("samples"))
|
|
108
134
|
# R1-C21: a well-formed but vacuous graph must announce itself — silence is what
|
|
109
135
|
# lets an unparsed layout read as a clean bill of health downstream.
|
|
110
136
|
for d in diagnostics(graph):
|
|
@@ -880,6 +906,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
880
906
|
help="Reuse an existing --out graph + its scope sidecar: recompute "
|
|
881
907
|
"only changed modules (R1-C9). Identical to a full build; much "
|
|
882
908
|
"faster on --deep. Single-package only (no --consumer/--docs).")
|
|
909
|
+
b.add_argument("--repeat", type=int, default=1, metavar="N",
|
|
910
|
+
help="--deep only: run the jedi layer N times and union the samples. "
|
|
911
|
+
"One deep build is one sample (a real edge was present in 75%% "
|
|
912
|
+
"of single builds, measured); an edge seen in fewer than N runs "
|
|
913
|
+
"carries extras.seen, provenance.samples records N. Costs N× the "
|
|
914
|
+
"jedi time. Not combinable with --incremental.")
|
|
883
915
|
b.set_defaults(func=_cmd_build)
|
|
884
916
|
|
|
885
917
|
sc = sub.add_parser("scope", help="Resolve the input scope manifest (scope_id + profile), or --diff two.")
|
|
@@ -23,6 +23,7 @@ UNREAD_INPUTS = "unread_inputs"
|
|
|
23
23
|
MODULE_COUNT_MISMATCH = "module_count_mismatch"
|
|
24
24
|
SCOPE_MEMBERSHIP = "scope_membership"
|
|
25
25
|
DEEP_TIER_UNSTABLE = "deep_tier_unstable"
|
|
26
|
+
INCREMENTAL_DEEP_SPLICE = "incremental_deep_splice"
|
|
26
27
|
|
|
27
28
|
#: A ``warning`` invalidates the conclusions a surface draws from the graph — read them as
|
|
28
29
|
#: unknown. A ``note`` states a fact about how the graph was built and invalidates nothing.
|
|
@@ -206,19 +207,86 @@ def deep_tier_diagnostic(graph) -> dict | None:
|
|
|
206
207
|
per-script execution budget — an inference that runs out of it returns nothing, and
|
|
207
208
|
that reads as `unresolved` rather than as an error.
|
|
208
209
|
"""
|
|
209
|
-
|
|
210
|
+
prov = graph.provenance or {}
|
|
211
|
+
if prov.get("tier") != "deep":
|
|
210
212
|
return None
|
|
213
|
+
# R1-C45: a reader deciding whether to trust an *absence* needs the per-edge share and
|
|
214
|
+
# what N does to it — "roughly one run in three" named the noise and not its remedy.
|
|
215
|
+
# The 75 % is stated as measured on one tree for one edge (126 of 168 full builds,
|
|
216
|
+
# issue #16; 5 of 8 here), never as a property of the tier: the consumer's own batches
|
|
217
|
+
# ranged from 1 of 7 to 7 of 7, and eight builds cannot see a 95 % edge at all.
|
|
218
|
+
samples = prov.get("samples") or {}
|
|
219
|
+
runs = samples.get("runs") or 1
|
|
220
|
+
if runs >= 2:
|
|
221
|
+
unstable = samples.get("unstable", 0)
|
|
222
|
+
residual = 0.25 ** runs
|
|
223
|
+
return {
|
|
224
|
+
"code": DEEP_TIER_UNSTABLE,
|
|
225
|
+
"severity": NOTE,
|
|
226
|
+
"tier": "deep",
|
|
227
|
+
"samples": runs,
|
|
228
|
+
"unstable": unstable,
|
|
229
|
+
"consequence": ("Edges carrying `extras.seen` are the ones the tier is unsure "
|
|
230
|
+
"about; everything else was present in every run."),
|
|
231
|
+
"message": (
|
|
232
|
+
f"union of {runs} deep (jedi) samples of one tree: {unstable} edge(s) "
|
|
233
|
+
f"were seen in fewer than {runs} runs and carry `extras.seen`. At the "
|
|
234
|
+
f"measured per-run rate (a real edge present in 75 % of single builds) "
|
|
235
|
+
f"an edge missed by all {runs} runs has probability about "
|
|
236
|
+
f"{residual * 100:.2g} %."
|
|
237
|
+
),
|
|
238
|
+
}
|
|
211
239
|
return {
|
|
212
240
|
"code": DEEP_TIER_UNSTABLE,
|
|
213
241
|
"severity": NOTE,
|
|
214
242
|
"tier": "deep",
|
|
243
|
+
"samples": 1,
|
|
215
244
|
"consequence": ("Everything here is a lower bound as usual; treat a difference "
|
|
216
|
-
"of a few
|
|
217
|
-
"noise rather than a change in the code
|
|
245
|
+
"of a few jedi-resolved edges between two deep graphs as tool "
|
|
246
|
+
"noise rather than a change in the code, and do not read a "
|
|
247
|
+
"missing call or attribute edge in one build as absence."),
|
|
218
248
|
"message": (
|
|
219
|
-
"built on the deep (jedi) tier, which is not byte-stable
|
|
220
|
-
"
|
|
221
|
-
"
|
|
249
|
+
"built on the deep (jedi) tier, which is not byte-stable — one build is one "
|
|
250
|
+
"sample. Measured on an 88-module tree, a real `accesses` edge was present in "
|
|
251
|
+
"126 of 168 full builds (75 %): one build misses such an edge about 1 time in "
|
|
252
|
+
"4, two builds 1 in 16, three 1 in 64. `build --repeat N` unions N samples "
|
|
253
|
+
"and marks what varied."
|
|
254
|
+
),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def incremental_splice_diagnostic(graph) -> dict | None:
|
|
259
|
+
"""Say when a deep graph carries regions this build did not recompute (R1-C43).
|
|
260
|
+
|
|
261
|
+
Sibling of :func:`deep_tier_diagnostic`, and the reason it is a separate code: that
|
|
262
|
+
one says *this graph is one sample*, which a reader can act on by building again.
|
|
263
|
+
This one says the sample is **frozen** — building again incrementally returns it
|
|
264
|
+
unchanged, so the standard remedy silently does nothing.
|
|
265
|
+
|
|
266
|
+
Measured on an 88-module package: from a graph missing one real `accesses` edge,
|
|
267
|
+
five consecutive incremental builds recovered it **0 times** while full builds of
|
|
268
|
+
the very same tree recovered it **5 of 5**. And the miss defends itself — the
|
|
269
|
+
invalidation rule that would have recomputed the writer reads the old graph, where
|
|
270
|
+
the edge is exactly what is absent (gaps/incremental_noise_persistence_2026-09-02.md).
|
|
271
|
+
|
|
272
|
+
Fast tier is excluded on purpose: there the splice is exact and byte-identity to a
|
|
273
|
+
full build is pinned by the suite, so there is no sample to freeze.
|
|
274
|
+
"""
|
|
275
|
+
prov = graph.provenance or {}
|
|
276
|
+
if prov.get("tier") != "deep" or not prov.get("incremental"):
|
|
277
|
+
return None
|
|
278
|
+
return {
|
|
279
|
+
"code": INCREMENTAL_DEEP_SPLICE,
|
|
280
|
+
"severity": NOTE,
|
|
281
|
+
"tier": "deep",
|
|
282
|
+
"consequence": ("Do not read a missing call or attribute edge here as evidence "
|
|
283
|
+
"that nothing depends on a symbol, and do not test that by "
|
|
284
|
+
"rebuilding incrementally — only a full rebuild resamples."),
|
|
285
|
+
"message": (
|
|
286
|
+
"parts of this deep graph were spliced from an earlier build rather than "
|
|
287
|
+
"recomputed: on this tier that carries the earlier build's jedi sample "
|
|
288
|
+
"forward, including anything it missed — measured at 0 recoveries in 5 "
|
|
289
|
+
"incremental builds against 5 of 5 full builds of the same tree."
|
|
222
290
|
),
|
|
223
291
|
}
|
|
224
292
|
|
|
@@ -307,7 +375,8 @@ def diagnostics(graph) -> list[dict]:
|
|
|
307
375
|
checks = (import_graph_diagnostic(graph), namespace_target_diagnostic(graph),
|
|
308
376
|
cross_root_diagnostic(graph), schema_diagnostic(graph),
|
|
309
377
|
unread_inputs_diagnostic(graph), module_count_diagnostic(graph),
|
|
310
|
-
scope_membership_diagnostic(graph), deep_tier_diagnostic(graph)
|
|
378
|
+
scope_membership_diagnostic(graph), deep_tier_diagnostic(graph),
|
|
379
|
+
incremental_splice_diagnostic(graph))
|
|
311
380
|
return [d for d in checks if d is not None]
|
|
312
381
|
|
|
313
382
|
|
|
@@ -78,6 +78,9 @@ def add_behavior(graph, griffe_root, target_pkg: str, *, deep: bool = False,
|
|
|
78
78
|
modmembers = set(mod.members.keys())
|
|
79
79
|
script = _jedi_script(source, fp, project) if deep else None
|
|
80
80
|
nested: dict[tuple[str, str], dict] = {}
|
|
81
|
+
# R1-C46: the surviving definition says which earlier bodies it replaced. Stamped
|
|
82
|
+
# here because this pass holds the AST; the walkers below skip those bodies.
|
|
83
|
+
_stamp_shadows(graph, tree, modpath)
|
|
81
84
|
for fnode, class_stack, scope in _named_functions_scoped(tree):
|
|
82
85
|
node_id = _node_id(modpath, class_stack, fnode.name)
|
|
83
86
|
local = local_imports.get(id(fnode))
|
|
@@ -349,17 +352,105 @@ def _class_members(mod, class_stack, modules) -> dict:
|
|
|
349
352
|
|
|
350
353
|
# -- ast scope walking -------------------------------------------------------
|
|
351
354
|
|
|
355
|
+
_DEF_NODES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
356
|
+
# Decorators that make a repeated binding of one name legitimate (R1-C46 / D2): an
|
|
357
|
+
# `@overload` *before* the implementation, a property accessor or a singledispatch
|
|
358
|
+
# registration *after* the first binding. Matched on the decorator's last dotted segment
|
|
359
|
+
# with any call stripped, so `typing.overload` and `@f.register(int)` both count.
|
|
360
|
+
_EXEMPT_EARLIER = frozenset({"overload"})
|
|
361
|
+
_EXEMPT_LATER = frozenset({"setter", "getter", "deleter", "register"})
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _decorator_tails(node) -> list[str]:
|
|
365
|
+
out = []
|
|
366
|
+
for d in node.decorator_list:
|
|
367
|
+
text = ast.unparse(d).split("(", 1)[0]
|
|
368
|
+
out.append(text.rsplit(".", 1)[-1])
|
|
369
|
+
return out
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _rebinding_is_legitimate(earlier, later) -> bool:
|
|
373
|
+
if later.name == "_":
|
|
374
|
+
return True # singledispatch convention
|
|
375
|
+
if any(t in _EXEMPT_EARLIER for t in _decorator_tails(earlier)):
|
|
376
|
+
return True
|
|
377
|
+
return any(t in _EXEMPT_LATER for t in _decorator_tails(later))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _shadow_map(tree) -> tuple[set[int], dict, list[tuple[int, int]]]:
|
|
381
|
+
"""Definitions that a later definition of the same name in the same body replaces.
|
|
382
|
+
|
|
383
|
+
R1-C46: griffe keeps the **last** body of a name defined twice and drops the first
|
|
384
|
+
without a word; the AST walkers here used to visit *both* and attribute both bodies'
|
|
385
|
+
calls to the one surviving node — an edge from code that can never run. So every
|
|
386
|
+
walker skips the shadowed subtree, and the surviving node records what it shadowed.
|
|
387
|
+
|
|
388
|
+
Only a *direct* statement of a module, class or function body shadows or is
|
|
389
|
+
shadowed. A definition under ``if`` / ``try`` / ``with`` is conditional on either
|
|
390
|
+
side (``if TYPE_CHECKING:``, an ``except ImportError`` fallback): it neither replaces
|
|
391
|
+
nor is replaced, and both bodies keep being walked — the honest over-approximation.
|
|
392
|
+
|
|
393
|
+
Returns ``(ids of AST nodes to skip, records, dead line ranges)``; ``records`` maps
|
|
394
|
+
``(scope chain, name) -> {"linenos": [...], "node": bool}`` where ``node`` says the
|
|
395
|
+
chain is classes only, i.e. the survivor is a definition node the graph holds.
|
|
396
|
+
"""
|
|
397
|
+
skip: set[int] = set()
|
|
398
|
+
records: dict[tuple[tuple[str, ...], str], dict] = {}
|
|
399
|
+
ranges: list[tuple[int, int]] = []
|
|
400
|
+
|
|
401
|
+
def walk(node, scope: list[str], kinds: list[str]) -> None:
|
|
402
|
+
if isinstance(node, (ast.Module, *_DEF_NODES)):
|
|
403
|
+
last: dict[str, ast.AST] = {}
|
|
404
|
+
for stmt in node.body:
|
|
405
|
+
if not isinstance(stmt, _DEF_NODES):
|
|
406
|
+
continue
|
|
407
|
+
prev = last.get(stmt.name)
|
|
408
|
+
if prev is not None and not _rebinding_is_legitimate(prev, stmt):
|
|
409
|
+
skip.add(id(prev))
|
|
410
|
+
ranges.append((prev.lineno, getattr(prev, "end_lineno", prev.lineno)))
|
|
411
|
+
rec = records.setdefault((tuple(scope), stmt.name),
|
|
412
|
+
{"linenos": [], "node": all(k == "c" for k in kinds)})
|
|
413
|
+
rec["linenos"].append(prev.lineno)
|
|
414
|
+
last[stmt.name] = stmt
|
|
415
|
+
for child in ast.iter_child_nodes(node):
|
|
416
|
+
if id(child) in skip:
|
|
417
|
+
continue
|
|
418
|
+
if isinstance(child, ast.ClassDef):
|
|
419
|
+
walk(child, scope + [child.name], kinds + ["c"])
|
|
420
|
+
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
421
|
+
walk(child, scope + [child.name], kinds + ["f"])
|
|
422
|
+
else:
|
|
423
|
+
walk(child, scope, kinds)
|
|
424
|
+
|
|
425
|
+
walk(tree, [], [])
|
|
426
|
+
return skip, records, ranges
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _stamp_shadows(graph, tree, prefix: str) -> None:
|
|
430
|
+
"""Write ``extras.shadows`` on every surviving definition node under ``prefix``."""
|
|
431
|
+
for (scope, name), rec in _shadow_map(tree)[1].items():
|
|
432
|
+
if not rec["node"]:
|
|
433
|
+
continue
|
|
434
|
+
nid = ".".join([prefix, *scope, name])
|
|
435
|
+
if nid in graph.nodes:
|
|
436
|
+
graph.nodes[nid].extras["shadows"] = sorted(rec["linenos"])
|
|
437
|
+
|
|
438
|
+
|
|
352
439
|
def _named_functions(tree):
|
|
353
440
|
"""Yield (FunctionDef, [class names]) for functions reachable as definitions.
|
|
354
441
|
|
|
355
442
|
Tracks the class nesting so we can rebuild canonical ids. Functions nested
|
|
356
443
|
inside other functions are still yielded but filtered out by the caller
|
|
357
|
-
(their id won't match a graph node).
|
|
444
|
+
(their id won't match a graph node). A definition shadowed by a later one of
|
|
445
|
+
the same name in the same body is not yielded (R1-C46, :func:`_shadow_map`).
|
|
358
446
|
"""
|
|
359
447
|
results = []
|
|
448
|
+
skip = _shadow_map(tree)[0]
|
|
360
449
|
|
|
361
450
|
def visit(node, class_stack):
|
|
362
451
|
for child in ast.iter_child_nodes(node):
|
|
452
|
+
if id(child) in skip:
|
|
453
|
+
continue
|
|
363
454
|
if isinstance(child, ast.ClassDef):
|
|
364
455
|
visit(child, class_stack + [child.name])
|
|
365
456
|
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
@@ -845,9 +936,12 @@ def _named_functions_scoped(tree):
|
|
|
845
936
|
because `dataflow`/`dispatch`/`attrflow` unpack the two-tuple.
|
|
846
937
|
"""
|
|
847
938
|
results = []
|
|
939
|
+
skip = _shadow_map(tree)[0] # R1-C46: a shadowed body is not walked
|
|
848
940
|
|
|
849
941
|
def visit(node, class_stack, scope):
|
|
850
942
|
for child in ast.iter_child_nodes(node):
|
|
943
|
+
if id(child) in skip:
|
|
944
|
+
continue
|
|
851
945
|
if isinstance(child, ast.ClassDef):
|
|
852
946
|
visit(child, class_stack + [child.name], scope + [child.name])
|
|
853
947
|
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
@@ -18,6 +18,7 @@ Emits (M0 + M1 + M1.5):
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import ast
|
|
21
|
+
import multiprocessing
|
|
21
22
|
import os
|
|
22
23
|
from dataclasses import dataclass, field
|
|
23
24
|
from pathlib import Path
|
|
@@ -29,6 +30,7 @@ from codemap.extract.behavior import add_behavior
|
|
|
29
30
|
from codemap.extract.dataflow import add_dataflow
|
|
30
31
|
from codemap.extract.dispatch import add_dispatch, add_family_links
|
|
31
32
|
from codemap.extract.gsource import NESTED_IMPORT_HINT, module_file, module_identity
|
|
33
|
+
from codemap.extract.union import merge_samples
|
|
32
34
|
from codemap.provenance import build_provenance
|
|
33
35
|
from codemap.model import Edge, Graph, Node
|
|
34
36
|
|
|
@@ -138,20 +140,61 @@ def add_behavioral_layer(graph, root, module_name, search_path, *, deep: bool,
|
|
|
138
140
|
only=attr_only)
|
|
139
141
|
|
|
140
142
|
|
|
141
|
-
def
|
|
143
|
+
def _sample_worker(package_path: str, deep: bool) -> dict:
|
|
144
|
+
"""One full extraction in a child interpreter; returns the graph as a dict (R1-C45)."""
|
|
145
|
+
graph, root, module_name, search_path = build_structural(package_path)
|
|
146
|
+
add_behavioral_layer(graph, root, module_name, search_path, deep=deep)
|
|
147
|
+
return graph.to_dict()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def collect_samples(package_path, *, deep: bool, runs: int,
|
|
151
|
+
workers: int | None = None) -> list[Graph]:
|
|
152
|
+
"""``runs`` independent samples of ``package_path``, each in a fresh interpreter.
|
|
153
|
+
|
|
154
|
+
Lives here, beside the two functions the worker calls, so ``extract/union.py`` stays
|
|
155
|
+
a pure merge and the import graph stays acyclic — codemap's own ``no_lazy_cycles``
|
|
156
|
+
contract caught the first draft, which reached back from ``union`` with a
|
|
157
|
+
function-local import. ``workers`` bounds concurrency (default: the machine's core
|
|
158
|
+
count, capped at ``runs``). Spawn, not fork: a forked child would inherit exactly the
|
|
159
|
+
state that makes an in-process repeat correlate with the pass before it.
|
|
160
|
+
"""
|
|
161
|
+
ctx = multiprocessing.get_context("spawn")
|
|
162
|
+
procs = max(1, min(runs, workers or os.cpu_count() or 1))
|
|
163
|
+
with ctx.Pool(processes=procs) as pool:
|
|
164
|
+
dicts = pool.starmap(_sample_worker, [(str(package_path), deep)] * runs)
|
|
165
|
+
samples = []
|
|
166
|
+
for d in dicts:
|
|
167
|
+
g = Graph.from_dict(d)
|
|
168
|
+
g.loaded_schema = None # a fresh build, not a file — keep schema_diagnostic quiet
|
|
169
|
+
samples.append(g)
|
|
170
|
+
return samples
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def extract(package_path: str | Path, *, deep: bool = False, repeat: int = 1) -> Graph:
|
|
142
174
|
"""Build a code graph from a Python package directory.
|
|
143
175
|
|
|
144
176
|
``deep=True`` runs the jedi-backed call resolver (M5) — richer call-graph
|
|
145
177
|
(local-variable type inference) at ~1 min build cost; default is the fast
|
|
146
178
|
ast tier (sub-second). See ``extract/behavior.py``.
|
|
179
|
+
|
|
180
|
+
``repeat=N`` (R1-C45) builds N samples — **each in a fresh interpreter**, the
|
|
181
|
+
regime whose recovery share was measured; in-process repeats come in correlated
|
|
182
|
+
streaks (see ``extract/union.py``) — and unions them: a deep build is one sample
|
|
183
|
+
of jedi's bounded inference, and an edge seen in fewer than N runs carries
|
|
184
|
+
``extras.seen``.
|
|
147
185
|
"""
|
|
148
|
-
|
|
149
|
-
|
|
186
|
+
if repeat > 1:
|
|
187
|
+
graph, stats = merge_samples(collect_samples(package_path, deep=deep, runs=repeat))
|
|
188
|
+
else:
|
|
189
|
+
graph, root, module_name, search_path = build_structural(package_path)
|
|
190
|
+
add_behavioral_layer(graph, root, module_name, search_path, deep=deep)
|
|
191
|
+
stats = {"runs": 1}
|
|
150
192
|
# R1-C25: even a library-built graph says which tool and which tier made it. The
|
|
151
193
|
# input identity (scope_id, source commit) is added by whoever resolved the scope —
|
|
152
194
|
# `extract` deliberately does not hash the tree a second time.
|
|
153
195
|
graph.provenance = build_provenance(tier="deep" if deep else "fast",
|
|
154
|
-
inputs=graph.provenance.get("inputs")
|
|
196
|
+
inputs=graph.provenance.get("inputs"),
|
|
197
|
+
samples=stats)
|
|
155
198
|
return graph
|
|
156
199
|
|
|
157
200
|
|
|
@@ -32,7 +32,7 @@ import os
|
|
|
32
32
|
import re
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
|
|
35
|
-
from codemap.extract.behavior import _arg_contract, _arg_shape
|
|
35
|
+
from codemap.extract.behavior import _arg_contract, _arg_shape, _shadow_map
|
|
36
36
|
from codemap.extract.griffe_extractor import extract
|
|
37
37
|
from codemap.provenance import canonicalize
|
|
38
38
|
from codemap.model import Edge, Graph, Node
|
|
@@ -78,18 +78,20 @@ def extract_repo(
|
|
|
78
78
|
docs: tuple[str | Path, ...] = (),
|
|
79
79
|
mode: str = "thin",
|
|
80
80
|
deep: bool = False,
|
|
81
|
+
repeat: int = 1,
|
|
81
82
|
) -> Graph:
|
|
82
83
|
"""Build a repo-scoped graph: core package + consumer roots + doc roots.
|
|
83
84
|
|
|
84
85
|
``core`` is analysed exactly as the single-package extractor (griffe, plus the
|
|
85
|
-
behavioral pass when ``deep
|
|
86
|
-
|
|
86
|
+
behavioral pass when ``deep``, unioned over ``repeat`` samples — R1-C45).
|
|
87
|
+
``consumers`` and ``docs`` are extra root directories scanned for references into
|
|
88
|
+
the core; those scans are deterministic and run once. ``mode`` is ``"thin"`` or
|
|
87
89
|
``"full"`` (see module docstring).
|
|
88
90
|
"""
|
|
89
91
|
if mode not in ("thin", "full"):
|
|
90
92
|
raise ValueError(f"mode must be 'thin' or 'full', got {mode!r}")
|
|
91
93
|
|
|
92
|
-
graph = extract(core, deep=deep)
|
|
94
|
+
graph = extract(core, deep=deep, repeat=repeat)
|
|
93
95
|
core_pkg = graph.target
|
|
94
96
|
for node in graph.nodes.values():
|
|
95
97
|
node.extras.setdefault("root", "core")
|
|
@@ -266,8 +268,14 @@ def _scan_consumer_module(graph, py, base, label, tree, index, mode, origin=None
|
|
|
266
268
|
if isinstance(n, ast.Attribute) and isinstance(n.value, ast.Attribute)
|
|
267
269
|
}
|
|
268
270
|
func_ranges = _func_ranges(tree) if mode == "full" else []
|
|
271
|
+
# R1-C46: a use inside a body that a later definition replaced is not a use — it
|
|
272
|
+
# would attribute a reference (or a call) to code that never runs, in either mode.
|
|
273
|
+
dead = _shadow_map(tree)[2]
|
|
269
274
|
|
|
270
275
|
for node in ast.walk(tree):
|
|
276
|
+
line = getattr(node, "lineno", None)
|
|
277
|
+
if line is not None and any(a <= line <= b for a, b in dead):
|
|
278
|
+
continue
|
|
271
279
|
target = None
|
|
272
280
|
use_node = node
|
|
273
281
|
if isinstance(node, ast.Name) and node.id in symbol_map:
|
|
@@ -375,14 +383,28 @@ def _materialize_defs(graph, tree, mod_id, label, file: str | None = None) -> No
|
|
|
375
383
|
extras={"root": label}))
|
|
376
384
|
parent = ".".join([mod_id, *class_stack]) if class_stack else mod_id
|
|
377
385
|
graph.add_edge(Edge("contains", parent, node_id))
|
|
386
|
+
# R1-C46: one node and one `contains` per definition that can run (issue #16 found
|
|
387
|
+
# the record twice); the survivor says which earlier bodies it replaced.
|
|
388
|
+
for (scope, name), rec in _shadow_map(tree)[1].items():
|
|
389
|
+
if rec["node"]:
|
|
390
|
+
nid = ".".join([mod_id, *scope, name])
|
|
391
|
+
if nid in graph.nodes:
|
|
392
|
+
graph.nodes[nid].extras["shadows"] = sorted(rec["linenos"])
|
|
378
393
|
|
|
379
394
|
|
|
380
395
|
def _defs(tree):
|
|
381
|
-
"""Yield (def-node, [enclosing class/def names]) for top-level & nested defs.
|
|
396
|
+
"""Yield (def-node, [enclosing class/def names]) for top-level & nested defs.
|
|
397
|
+
|
|
398
|
+
A body shadowed by a later definition of the same name in the same scope is not
|
|
399
|
+
yielded (R1-C46, :func:`_shadow_map`).
|
|
400
|
+
"""
|
|
382
401
|
results = []
|
|
402
|
+
skip = _shadow_map(tree)[0]
|
|
383
403
|
|
|
384
404
|
def visit(node, stack):
|
|
385
405
|
for child in ast.iter_child_nodes(node):
|
|
406
|
+
if id(child) in skip:
|
|
407
|
+
continue
|
|
386
408
|
if isinstance(child, ast.ClassDef):
|
|
387
409
|
results.append((child, list(stack)))
|
|
388
410
|
visit(child, stack + [child.name])
|
|
@@ -399,9 +421,12 @@ def _defs(tree):
|
|
|
399
421
|
def _func_ranges(tree):
|
|
400
422
|
"""(start, end, node_id) for each def, longest-first, to place a use by line."""
|
|
401
423
|
ranges = []
|
|
424
|
+
skip = _shadow_map(tree)[0] # R1-C46: a shadowed body owns no uses
|
|
402
425
|
|
|
403
426
|
def visit(node, mod_stack):
|
|
404
427
|
for child in ast.iter_child_nodes(node):
|
|
428
|
+
if id(child) in skip:
|
|
429
|
+
continue
|
|
405
430
|
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
406
431
|
nid = ".".join([*mod_stack, child.name])
|
|
407
432
|
end = getattr(child, "end_lineno", child.lineno)
|