Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli

## [Unreleased]

### Changed — Python imports use the shared capture vocabulary (#358, Python slice)

Python dependency targets now come from `@import.path`; named clauses and their optional local
renames use `@import.names` and `@import.alias` to produce the existing import bindings.
The Python branches and binding extractor they replace leave `src/ingest_relations.h`.
Resolution stays unchanged. A comma-separated plain import still contributes only its first
dependency target, and an aliased first clause retains its written `as` spelling in `--deps`.
These existing limits are preserved rather than widened by the capture migration.

The Python walk is removed. Captured targets and bindings retain the same container reach and depth limit:
imports at 255/256 containers remain, and a real import at 257 is cut and disclosed. An import-free deep
container, including a top-level import followed by a deep `pass`, no longer produces `extract-partial`
and may be cached. Disclosure now reports a captured import actually cut, rather than an empty container.
No persisted record or extraction-version change is intended; the Python import gate checks
fresh/warm output and, when `RIPWIRE_BASE_BIN` is supplied, acceptance of a pristine-main cache
without rewriting it.


## [0.6.7] — 2026-10-11

### Fixed — `--impact`: a selector matching several definitions keeps the `via="name"` hedge in its per-file rollup
Expand Down
19 changes: 19 additions & 0 deletions queries/python/tags.scm
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,22 @@
(attribute
attribute: (identifier) @name)
]) @reference.call

; Shared import vocabulary (#358). The dependency target is still ONE per statement: the first
; name of a plain import (including its alias spelling), or the module_name of a from-import.
; The child anchor captures only the first plain-import name. C++ imposes the old container
; reach/depth; statement patterns remain unanchored so conditional and function imports keep coverage.
(import_statement . name: (_) @import.path)
(import_from_statement module_name: (_) @import.path)

; Every named clause binds, even when it is not the first dependency target. @import.names is
; one member of that clause list per match; @import.alias is its optional local rename. Wildcards
; bind no name here, as before. Keep aliases in the SAME match as their named target.
(import_statement name: (dotted_name) @import.names)
(import_statement name: (aliased_import
name: (dotted_name) @import.names
alias: (identifier) @import.alias))
(import_from_statement name: (dotted_name) @import.names)
(import_from_statement name: (aliased_import
name: (dotted_name) @import.names
alias: (identifier) @import.alias))
63 changes: 54 additions & 9 deletions src/ingest_importcap.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@
// the bare name `require`, whether an argument is a single string literal. Those are normaliser arms, not
// extractors — one per dialect, not one per language.
//
// THE VOCABULARY IS A FAMILY, AND ONLY ITS FIRST MEMBER IS SPENT. `@import.path` is the only capture name
// in use, because C-family imports have no other part: `#include` names a path and nothing else — no
// THE VOCABULARY IS A FAMILY. Python is the first dialect to use `@import.names` / `@import.alias`
// for bindings only, never extra dependency targets. The names remain reserved for later dialects.
// `@import.path` is the dependency-target capture, because C-family imports have no other part: `#include` names a path and nothing else — no
// alias to record, no list of imported names — and because Go's alias/dot/blank spellings ride inside the
// declaration clause its Include has always carried (see DepDialect::Go below), not in a field of their own.
// The other two names the issue sketches are reserved BY NAME so the next language lands on them rather
// than inventing a spelling: `@import.alias` is the local name an import renames a target to (`import a.b as c`, `use Foo\Bar as Baz`, `using X = Foo.Bar`), and
// `@import.names` is a group of targets ONE directive names (`use Foo\{A, B}`, `alias MyApp.{Bar, Baz}`),
// which is why both of those are emitted by the C++ side today through a per-directive helper rather than
// by one capture — a capture names one node, and a group is many. No enum value reserves them: an unused
// arm that no query can reach is a lie about what the vocabulary supports.
// `@import.alias` is the local name an import renames a target to (`import a.b as c`, `use Foo\Bar as Baz`, `using X = Foo.Bar`).
// `@import.names` names the imported clauses of a directive (`use Foo\{A, B}`, `alias MyApp.{Bar, Baz}`).
// Python emits one names clause and its optional alias in the SAME match, then its Python-specific
// helper produces the existing RawBind. Later dialects may need a per-directive helper for groups;
// a capture names one node, and a group is many. No unused enum arm pretends to support a dialect.
//
// SECOND MEMBER: DepDialect::Web (TypeScript, TSX, JavaScript, the .astro frontmatter that rides TypeScript).
// Its three spellings are three patterns on the SAME capture name — `import … from 'x'`, a re-export
Expand Down Expand Up @@ -228,7 +228,7 @@ ImportSpec normaliseGoImport( TSNode declaration, std::string_view src )
}

// ── the dispatch: ONE normaliser per DepDialect ──────────────────────────────────────────────────────
// CFamily, Web and Go are the dialects that have moved over so far; every other arm returns an empty ImportSpec,
// CFamily, Web, Go and Python are the dialects that have moved over so far; every other arm returns an empty ImportSpec,
// which is what makes the capture INERT rather than wrong on a language whose tags.scm has not adopted
// `@import.path` yet. Each `case` is deleted as its language moves, and the compiler then names every
// other dialect still to do — the same "one language at a time" landing the issue asks for.
Expand All @@ -239,6 +239,7 @@ ImportSpec normaliseImportSpecifier( DepDialect dialect, TSNode directive, std::
case DepDialect::CFamily: return normaliseCFamilyImport( directive, raw, src );
case DepDialect::Web: return normaliseWebImport( directive, raw, src );
case DepDialect::Go: return normaliseGoImport( directive, src );
case DepDialect::Python: return { std::string( raw ) }; // preserve the first plain clause's written alias
default: return {}; // no other language captures @import.path yet
}
}
Expand All @@ -253,6 +254,7 @@ inline bool importsFromCapture( DepDialect dialect ) noexcept
case DepDialect::CFamily:
case DepDialect::Web:
case DepDialect::Go:
case DepDialect::Python:
return true;
default:
return false;
Expand All @@ -277,6 +279,49 @@ TSNode importDirectiveOf( DepDialect dialect, TSNode pathNode ) noexcept
return directive;
}

// One @import.names clause and its optional @import.alias → the existing Python Import binding.
// The query selects the clause; normalization keeps the plain-import module marker and from-import
// member distinction (#287). Both records stay at the STATEMENT byte, with the same empty spans.
// A multi-clause import still has only its FIRST Include, but every clause binds a name. Reach and
// depth must apply to bindings too, or a cut/ERROR-wrapped import would still narrow a call.
void emitCapturedPythonImportBind( TSNode namesNode, TSNode aliasNode, std::uint32_t fileId, std::string_view src, std::vector<RawBind>& binds )
{
const bool hasAlias = !ts_node_is_null( aliasNode );
const TSNode clause = hasAlias ? ts_node_parent( namesNode ) : namesNode;
const TSNode stmt = ts_node_parent( clause );
const bool isFrom = kindIs( ts_node_type( stmt ), "import_from_statement" );
const ImportReach reach = importContainerReach( stmt, Lang::Python );
if( reach.depth > kMaxImportContainerDepth || !reach.reachable )
{
return;
}
// The old walk called its binding helper only after extracting a nonempty first target.
const TSNode first = fieldChild( stmt, isFrom ? NodeField::ModuleName : NodeField::Name );
if( nodeTextOf( first, src ).empty() )
{
return;
}
std::string_view bound = nodeTextOf( hasAlias ? aliasNode : namesNode, src );
if( !isFrom && !hasAlias )
{
bound = bound.substr( 0, bound.find( '.' ) ); // `import a.b` binds `a`, not `a.b`
}
std::string target = importSpecifierText( isFrom ? first : namesNode, src );
if( bound.empty() || target.empty() )
{
return;
}
RawBind b;
b.fileId = fileId;
b.startByte = ts_node_start_byte( stmt );
b.lang = Lang::Python;
b.kind = LocalBindKind::Import;
b.var.assign( bound );
b.typeName = std::move( target );
b.importedName = isFrom ? std::string{} : "module";
binds.push_back( std::move( b ) );
}

// One captured `@import.path` → the Include record plus its ABS-3 import-role use-site ref, which is
// exactly the pair captureIncludes' `emitDirective` lambda minted for a C-family directive.
//
Expand Down
Loading
Loading