from __future__ import … is silently absent from get_dependencies — the python walker has no arm for future_import_statement and nothing in the tree records a decision #176
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#176
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while authoring hand-verified benchmark questions for #51. Traced to source.
Measured
A four-line Python file:
Three import statements, two reported, no disclosure of the third.
On the pinned
python-flaskcorpus this was the entire discrepancy in a hand-verified question:get_dependencies("src/flask/sansio/scaffold.py")reportsimports.total: 16against a hand-counted 17. All sixteen others are present; the only missing one isscaffold.py:1,from __future__ import annotations.Mechanism (read at source)
tree-sitter-python parses
from __future__ import Xas afuture_import_statementnode, which is a distinct node type fromimport_from_statement.crates/plugins/src/python.rs's walker has arms forimport_statementandimport_from_statementonly. Afuture_import_statementmatches neither, so the walk never reachesemit_from_importand no import row is produced.Why this is filed as a defect rather than corrected in the oracle
The neighbouring case in the same batch went the other way, and the contrast is the argument. C#'s
nameof(X)is also excluded from refs — but that exclusion is decided: there is a comment at the site, a test namednameof_is_not_a_call_ref, and a migration (m0028) recording it. So the benchmark oracle was corrected to match, because the tool is doing what it says.For
__future__there is nothing. I searched the tree for__future__and forfuture_import: no code path, no comment, no test, no migration, no doc line. There is no decision to defer to — only an arm that was never written. And the reply discloses no omission, while the tool promises "the imports the file declares".Two states rendering identically: "we deliberately do not model this" and "we never saw this node type". That is the house failure mode, and the only way to tell them apart here was to grep for a decision and find none.
Why existing gates could not see it
future_import_statementis not among them, so nothing compares the node type to a walker arm.corpus_ratchetpinsimportsas an exact count per repo.python-flaskrecords 650, and it has been consistently 650 including every missing__future__line since the baseline was first taken. A ratchet cannot see a row that was never emitted in any recorded run.get_dependencies's own e2e coverage uses fixtures written by hand, and nobody writing an import fixture reaches forfrom __future__ import annotations.Repro
A file containing exactly the three lines above, indexed, then
get_dependencieson it. Or on the pinnedpython-flaskcorpus (sha36e4a824f340fdee7ed50937ba8e7f6bc7d17f81):get_dependencies("src/flask/sansio/scaffold.py")→ 16, againstrg -c '^from |^import ' src/flask/sansio/scaffold.py→ 17.What must NOT be done to make this pass
__future__should be excluded — there is a real argument, since it is a compiler directive rather than a dependency — then that needs the same three artifactsnameofhas: a comment, a test, and a note. An exclusion nobody wrote down is indistinguishable from a bug, which is exactly why this issue exists.future_import_statementalone. The finding is that a node type was missed, and the same class of miss can exist for other Python statement forms. What would close this properly is a check that the walker's arms cover the node types the grammar can produce at that position — the kind of registry this repo already uses elsewhere — rather than one more arm.python-flask'simports: 650as the way to land it. That count moving is the evidence the fix worked, and it must be blessed with a reason naming this cause, not absorbed as drift.Measured vs inferred
The four-line fixture reading, the flask 16-vs-17, the node type, the walker's arm list, and the absence of any
__future__mention in the tree are all measured or read at source. Nothing here is inferred.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
FIXED, with the registry the issue asked for — and the registry immediately found a second instance of the same class in TypeScript.
The real node shape (dumped, not assumed)
Worth pasting because it settles a real trap:
emit_from_importreused blind would readmodule_nameasNone, default the prefix to empty, and spell the moduleannotations. The name children (dotted_name,aliased_import) are the same shapes thefromarm already handles, so the fix is a module-prefix override rather than a second emitter.The decision, made explicitly and written down
from __future__ import annotationsis a dependency the file declares. It is an import statement,get_dependenciespromises "the imports the file declares", andimportlibsees it. So it is emitted, with the same three artifactsnameofhas — a comment at the site, a test, and this note:plus an
import-kind ref qualified on__future__, exactly asfrom collections import defaultdictalready produces one qualified oncollections.The registry gate —
crates/plugins/tests/import_node_kind_registry.rsRule: for each graded grammar, every NAMED, non-hidden node kind whose name carries the language's import spelling as a
_-separated segment (or its plural) must carry a verdict —Reached{probe, expect},NotAnImportStatement{why}, orKnownGap{why}.Two properties make it more than a checklist:
Language::node_kind_count/node_kind_for_id/node_kind_is_named), so a grammar bump that introduces a new import kind fails the build rather than being noticed later.plugin.extract()on a probe and pinning the exact import rows — not by grepping the source for arm strings. That was measured, not assumed: with the arm deleted,grep -c future_import_statement python.rsstill printed 4, because the comment mentions it. A grep-form registry stays green through the very mutation it exists for.Segment-not-substring is load-bearing: rust's
else_clause/where_clauseand php'sbase_clause/catch_clause/class_interface_clause/finally_clauseall containuse.Bound, stated so it is not read as more than it is. 7 grammars (python, rust, c#, php, ts, tsx, js), 48 (grammar, kind) entries. Deliberately NOT covered, each with a written reason in the file: ruby (no import-spelled kind at all —
require/load/includeare method calls; asserted empty by its own test so a future grammar reddens the residual instead of hiding it); every other statement kind (the broad version of this rule is the one that gets discarded whole); whether an emitted module STRING is correct; ref rows.The registry found a second instance of this exact defect class
TypeScript
import A = require('m')(import_require_clause, ts+tsx) emits no import row at all. Same shape as this issue — a distinct grammar node kind, no walker arm, no disclosure, no decision anywhere. Registered as the oneKnownGap, with aknown_gaps_are_still_gapstest that goes RED when it is fixed, so promoting it is forced rather than optional. This is worth its own issue.Also recorded as a reason rather than fixed: php
class C { use T; }(trait composition) produces neither an import row nor a type ref — the trait edge is invisible to the graph. A recall finding aboutphp.rs.Mutations — four run, real RED
1. DATA, delete the arm →
6 passed; 3 failed2. PREDICATE matches NOTHING (
has_import_spelling→false) →7 passed; 2 failed3. PREDICATE matches EVERYTHING (segment →
contains) →6 passed; 3 failed4. PROBE VACUITY (swap the probe for
import __future__, which emits an import row but never produces the kind) →8 passed; 1 failedBoth predicate directions were mutated, per the #178 lesson. All restores
cp+ md5-verified +touched; nogit checkout.Corpus deltas — MEASURED, EXPLAINED, and NOT BLESSED
COSI_CORPUS_DIR=… COSI_CORPUS_REQUIRE=1, non-vacuous (7 repos indexed):importsrefsresolvedEvery other repo unchanged by this edit — attribution proved by isolation, not asserted: with
python.rsreverted to its pre-fix snapshot the ratchet reports python-flask at baseline and onlycs-dappermoving, which is #172's work.+27 is exact and explained:
grep -rc "from __future__ import"over the pinned checkout = 27 lines in 27 files, one import row + one import ref each.resolveddid not move — the new refs are unresolved qualified (__future__), so zero new phantoms, consistent with how every other stdlib from-import already behaves.src/flask/sansio/scaffold.pymeasured directly:imports.total16 → 17, matching the hand-count.tests/corpus/baseline.jsonis byte-identical. Per this lane's brief, a record that moves is a finding to report rather than something to bless, so the exact number and its cause are above and the record is untouched. This is the one thing left for the owner to action.Second artifact this moves, also measured and also unblessed —
tests/bench/ratchet.jsonpython-flask:correct29 → 30,recall0.8788 → 0.9091,tool_tokens7159 → 7192 (1.005x, inside the 0.70–1.05 band).flask.dependencies.sansio_scaffoldwas pinned atequals: 17as a product defect and now returns 17 with no MISSED. Recall is a floor, so the suite stays green in the safe direction.Gates
rustfmt --check(edition 2021) 0 ·clippy -p code-index-plugins --all-targets -D warnings0 ·cargo test -p code-index-plugins0 (270 lib + 9 registry) ·RUSTDOCFLAGS="-D warnings" cargo doc0 ·doc_citation_gate0 ·precision_gate0 — 7/7, phantoms=0 in every language · corpus ratchet 101, the +27 above, by design.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
CLOSING — fixed, and closed the way this issue asked rather than the way it forbade
Close-out lane. Verified on master
552e3a2; code read with this repo's own tools.On master
crates/plugins/src/python.rs:266— the missing arm exists:preceded by a 25-line comment at
:241-265that dumps thefuture_import_statementnode shape, states the decision explicitly (__future__is a dependency —importlibcan load it), and names the gate. That is the third artifact this issue demanded: a recorded decision, so the next reader is not left inferring one from an absent arm.Graded by a registry, which is what the issue asked for instead of one more arm
crates/plugins/tests/import_node_kind_registry.rs— 9 tests, all passing (EXIT=0 here). It discovers import node kinds from the compiled grammar rather than from a hand-written list, and proves reachability by actually runningplugin.extract()— so a future grammar bump that introduces another unhandled import kind fails the gate instead of going silent, which is the generalisation this issue explicitly preferred over a per-case fix.tests/corpus/baseline.jsonmoved and was blessed with the exact attribution —+27 / 27 lines in 27 files,resolvedunmoved — rather than blessed to make something pass.Two gaps the registry itself surfaced, which belong in new issues rather than here
Recording them so they are not lost when this closes:
import A = require('m')—import_node_kind_registry.rs:574registers it (import_require_clause, ts + tsx) as aKnownGap: "DOES declare a dependency onmand the extractor emits nothing — the same defect class as #176, in a second language." At least protected byknown_gaps_are_still_gaps.class C { use T; }produces neither an import row nor a type ref, so the trait edge is invisible to the graph. Protected by nothing found in the tree.Closing.