Skip to content
Open
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli

## [Unreleased]

### Changed — C# using directives share the import capture path (#358, slice 1a, @LaimaWu)

C# `using` directives now use `@import.path` and the CSharp normaliser; the old `csharpUsingTarget`
and `directiveTargetOf` branch are removed. Targets, directive sites, order, duplicate directives,
static/global forms and existing binding behaviour are preserved. The known alias gap stays pinned:
`using X = Foo.Bar` still reports X; alias bindings belong to slice 2b after the shared binds channel (2a).

As with the Python capture move (#382), containers beyond depth 256 without a deeper import no longer
make a file `extract-partial` or prevent whole-cache reuse. A nonempty import beyond the bound still
discloses the cut and is not captured. `csharpcheck` arms C6/C7 pin the boundary and the new mechanism.
Parser tag 214 (rich 215) invalidates the old extraction cache; the record format (36) is unchanged. Thanks to @LaimaWu for the change.

### Added — `--for` names the members of its shown owner rows on the compact route (`mem=`)

On a conceptual `--for` answer (root `bundle="compact"`), a shown class, struct or interface/trait row now carries
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ review will credit it, but it is never a condition for merging the language.

| Family | Shared mechanism today | State | Claimable slices |
|---|---|---|---|
| 1. Imports | `ingest_importcap.h`: `@import.path` captures, `emitCapturedImport`, one `normaliseImportSpecifier` arm per `DepDialect`. Resolution in `resolve.h`. | partly. C-family, JS/TS, Go and Python read the capture (Python since #382). The rest still go through per-language arms of `directiveTargetOf` (`ingest_relations.h`): `csharpUsingTarget`, `phpUseTarget`, `luaRequireTarget`, `bashSourceTarget`, `rubyRequireTarget`, `elixirDirectiveTarget`, plus Rust `use`/`mod`, Kotlin `import_header` and Swift `import_declaration`. | **1a** one language per PR onto `@import.path` plus its normaliser arm, deleting its arm in `directiveTargetOf`: S each for C#, PHP, Lua, Bash, Kotlin and Swift; M for Rust (`use` trees, `mod`; **wanted**), Ruby (`autoload`) and Elixir (`alias`/`use`), whose directives carry more meaning. Claim on #358. |
| 1. Imports | `ingest_importcap.h`: `@import.path` captures, `emitCapturedImport`, one `normaliseImportSpecifier` arm per `DepDialect`. Resolution in `resolve.h`. | partly. C-family, JS/TS, Go, Python and C# read the capture (Python since #382). The rest still go through per-language arms of `directiveTargetOf` (`ingest_relations.h`): `phpUseTarget`, `luaRequireTarget`, `bashSourceTarget`, `rubyRequireTarget`, `elixirDirectiveTarget`, plus Rust `use`/`mod`, Kotlin `import_header` and Swift `import_declaration`. | **1a** one language per PR onto `@import.path` plus its normaliser arm, deleting its arm in `directiveTargetOf`: S each for PHP, Lua, Bash, Kotlin and Swift; M for Rust (`use` trees, `mod`; **wanted**), Ruby (`autoload`) and Elixir (`alias`/`use`), whose directives carry more meaning. Claim on #358. |
| 2. Aliases | None. Renames reach the resolver as `RawBind` records (`ingest_cache.h`) from separate producers: Python's `@import.names` / `@import.alias` captures, read in `captureTagsFacts` (`ingest_sidecap.h`) and turned into binds by the Python-only `emitCapturedPythonImportBind` (`ingest_importcap.h`); `captureJsImportFacts`, `captureJsModuleAliases` and `captureGoImportFacts` (`ingest_jsimports.h`), C/C++ aliases in `fnPtrAliasName` (`ingest_binds.h`) and `captureTypeAlias` (`ingest_relations.h`). The readers are separate too: `addTypeAliasBases` and `buildUsingReexports` (`resolve.h`). | duplicated: one "local name → target" concept in about five shapes. | **2a** one shared binds channel (`@import.alias` / `@import.names` → one `RawBind` kind): M, *maintainers*, after the #358 import slices. **2b** after 2a, one language per PR moves its renames onto it: S each (Rust `use a as b`, **wanted**; Kotlin `import x as y`, PHP `use … as`, C# `using X = …`). |
| 3. Types | `captureTagsFacts` (`ingest_sidecap.h`) reads `@definition.*` captures for every language. | shared, but language decisions are `Lang::` branches inside the loop: 36 non-comment `Lang::` mentions, 15 of them Elixir. | **3a** #374: one constexpr per-language hook table that `captureTagsFacts` calls once per capture, one language per commit, Elixir first. M. |
| 4. Inheritance | `captureBases` (`ingest_relations.h`): base clauses for C++, TS/JS, Java, Python, Swift, C#, PHP, Kotlin and Ruby, chosen by node kind (`isBaseTypeNodeIn`). | partly. Rust `impl … for` is a separate pass; Ruby mixins read the same path through `@reference.mixin` captures since slice 4a (#400). | **4a** done (#400): Ruby `include`/`extend`/`prepend` read the shared path through `queries/ruby/tags.scm` captures, `captureRubyMixinBases` retired. **4b** the base-clause node kinds move from `isBaseTypeNodeIn` into `tags.scm` captures: M, all of the languages above. **4c** structural conformance (#386): a class or type whose methods cover a structural base's required methods (Python `typing.Protocol`, Go interfaces, possibly TS interfaces) is listed as an implementor marked `structural=1`, never as a nominal subclass, with a disclosed minimum-method threshold; step (c), one shared method-set check, per-language data for which bases are structural. M; Python and Go first (Go **wanted**). |
Expand Down
2 changes: 1 addition & 1 deletion prompts/help-wanted/visual-basic-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ Start this only after STEP 0 clears the bar. It follows the path Dart took (comm

### STEP 5: relations, in a separate round

- **`Imports`.** VB `Imports` is C#'s `using`; see `csharpUsingTarget` and
- **`Imports`.** VB `Imports` is C#'s `using`; see `normaliseCSharpImport` in `src/ingest_importcap.h` and
`kCsharpImportContainers` in `src/ingest_relations.h`. C# is deliberately absent from the
import-narrowing table in `src/resolve.h`, because a namespace has no one-to-one file.
`test/csharpcheck.sh`'s header explains why. Do the same for VB: capture `Imports` for `--uses`
Expand Down
8 changes: 6 additions & 2 deletions queries/csharp/tags.scm
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
; - type declarations: class / struct / interface / record / enum -> def nodes (the containers)
; - method / constructor / property declarations -> the def nodes calls resolve TO
; - invocations + object creations -> the call references (edges)
; - `using` directives -> captured separately by ingest.cpp::captureIncludes (NOT here — matches
; how every other language's imports are handled: tags.scm never captures imports directly).
; - `using` directives -> shared @import.path plus the CSharp normaliser; alias bindings remain
; out of scope (slice 2b after the shared binds channel, 2a).
;
; Deliberately NOT captured (noise, matching Java/Ruby): fields, local variables, attributes.

; Capture the whole directive, as Go does: the normaliser preserves the old target selection and
; both Include and Import RawRef stay sited at the directive, including a leading `global`.
(using_directive) @import.path

; ---- definitions ----

(class_declaration
Expand Down
6 changes: 5 additions & 1 deletion src/ingest_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,11 @@ constexpr std::uint32_t kCacheVersion = 36; // 36: train 30 — strict
// (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) —
// a target FORMAT change → old caches must be rejected.
// 4: Include gained a `bool isAngle` (quote/angle) field
constexpr std::uint32_t kParserVer = 212; // bump on any grammar/.scm/extraction change
constexpr std::uint32_t kParserVer = 214; // bump on any grammar/.scm/extraction change
// 214 = 2026-10-21 (#358 1a, PR #420, merged with main 320f8d8f): C# using directives move to shared
// captures; import-free deep containers no longer disclose a cut or lose whole-cache eligibility.
// Alias/edge facts unchanged. Reserved for #420 above main 206 (rich 207) and the train-32 numbers
// 212/213 (32a); train 32b goes above. Rich file tag 215. kCacheVersion stays 36 (no record format change).
// 212 = 2026-10-21 (train 32a: member-names + rust-receivers): strictly above every live number — main 206 (rich 207),
// lane rust-receivers 202 (203), lane ix1-slot 204 (205), lane l6-iface 210 (211); #420 reserves 214 (215), so train
// 32b can go above. Rich file tag 213. kCacheVersion stays 36: neither lane changed a record format (rust-receivers
Expand Down
24 changes: 21 additions & 3 deletions src/ingest_importcap.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,24 @@ ImportSpec normaliseGoImport( TSNode declaration, std::string_view src )
return out;
}

// ── DepDialect::CSharp ───────────────────────────────────────────────────────────────────────────────
// Preserve the walk's target selection, including its known alias gap: with the pinned grammar,
// `using X = Foo.Bar` has no `type` field, so the first matching child is X. Fixing that or emitting
// alias bindings belongs to 2b, after the shared binds channel (2a). Namespaces remain deferred in resolve.h.
ImportSpec normaliseCSharpImport( TSNode directive, std::string_view src )
{
// Grammar-bump insurance: never taken on the pinned grammar (an alias directive has no `type` field), no arm pins it.
if( const TSNode aliasType = fieldChild( directive, NodeField::Type ); !ts_node_is_null( aliasType ) )
{
return { importSpecifierText( aliasType, src ) };
}
// One cursor scan, including comment-heavy directives (childwalkscalecheck B16).
const TSNode c = firstChildOfKind( directive, /*namedOnly=*/true, { "qualified_name", "identifier", "generic_name", "alias_qualified_name" } );
return { ts_node_is_null( c ) ? std::string {} : importSpecifierText( c, src ) };
}

// ── the dispatch: ONE normaliser per DepDialect ──────────────────────────────────────────────────────
// CFamily, Web, Go and Python are the dialects that have moved over so far; every other arm returns an empty ImportSpec,
// CFamily, Web, Go, Python and CSharp 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 @@ -240,6 +256,7 @@ ImportSpec normaliseImportSpecifier( DepDialect dialect, TSNode directive, std::
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
case DepDialect::CSharp: return normaliseCSharpImport( directive, src );
default: return {}; // no other language captures @import.path yet
}
}
Expand All @@ -255,6 +272,7 @@ inline bool importsFromCapture( DepDialect dialect ) noexcept
case DepDialect::Web:
case DepDialect::Go:
case DepDialect::Python:
case DepDialect::CSharp:
return true;
default:
return false;
Expand All @@ -266,9 +284,9 @@ inline bool importsFromCapture( DepDialect dialect ) noexcept
// specifier sits in the call's `arguments`.
TSNode importDirectiveOf( DepDialect dialect, TSNode pathNode ) noexcept
{
if( dialect == DepDialect::Go )
if( dialect == DepDialect::Go || dialect == DepDialect::CSharp )
{
return pathNode; // the Go capture IS the whole import_declaration
return pathNode; // these captures ARE whole directives
}
const TSNode parent = ts_node_parent( pathNode );
const bool hasParent = !ts_node_is_null( parent );
Expand Down
46 changes: 6 additions & 40 deletions src/ingest_relations.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// 2026-08-29 split: base-clause type references (isBaseTypeNode/emitBaseRef/captureBases with the
// Rust impl walk), function-like #define body call edges (the macro-edges round), field/composition
// capture (captureFields), and the import/include layer — importName and the per-language
// specifier readers (C# using, PHP use, JS module loads, preproc include/import), the
// specifier readers (PHP use, JS module loads, preproc include/import; C# using moved to ingest_importcap.h in #358), the
// import-container tables by language, directiveTargetOf and captureIncludes. Everything that turns
// one file's AST into edges BETWEEN symbols and files. Same contract as every ingest_*.h: reopens
// `namespace rw` and the unnamed namespace inside it — one TU, one unnamed namespace, internal
Expand Down Expand Up @@ -1548,41 +1548,12 @@ inline std::string importSpecifierText( TSNode node, std::string_view src )
return std::string( s );
}

// C# `using Foo.Bar;` / `using static Foo;` / `using X = Foo.Bar;` target extraction, factored out of
// captureIncludes (kept as its own def to hold captureIncludes' own complexity/LOC down — one AST shape
// per language stays a one-line call at the use site, matching the existing per-branch grain there).
//
// C# namespaces do not map 1:1 onto files (unlike a Python module or a Rust `mod`), so — same as Go/
// Swift below — this is captured for --uses/--deps visibility only; resolve.h's Rule 3 precise-import
// narrower (includeLangOf) has no C# entry and DEFERS (kNoFile), matching Java's existing "Other"
// treatment: the conservative fall-through the P2-D Rule 3 contract requires.
//
// The ALIAS form `using X = Foo.Bar;` exposes a `name` field, but it names the ALIAS (`X`), not the
// aliased type — walk the `type` field instead so the target is the real aliased spelling. The plain/
// `static` form (`using Foo.Bar;` / `using static Foo;`) has NO named field at all: the grammar's
// `$._name` production is hidden-inlined, so its resolved concrete node (a qualified_name for a dotted
// path, else a bare identifier/generic_name) is just an unnamed-field child of using_directive — scan
// for the first one.
inline std::string csharpUsingTarget( TSNode usingNode, std::string_view src )
{
if( const TSNode aliasType = fieldChild( usingNode, NodeField::Type ); !ts_node_is_null( aliasType ) )
{
return importSpecifierText( aliasType, src );
}

// O(children): `using /*…*/ System.Text;` puts the comments in the using_directive itself — 16 000 of
// them measured 36x the identical flood inside a method body (test/childwalkscalecheck.sh, arm B16)
const TSNode c = firstChildOfKind( usingNode, /*namedOnly=*/true, { "qualified_name", "identifier", "generic_name", "alias_qualified_name" } );
return ts_node_is_null( c ) ? std::string {} : importSpecifierText( c, src );
}

// csharpUsingTarget's PHP sibling: the written specifier of a `use` statement. Node shapes read off
// PHP: the written specifier of a `use` statement. Node shapes read off
// tree-sitter-php v0.24.2's node-types.json and confirmed on a real parse:
// use Foo\Bar; namespace_use_declaration > namespace_use_clause > qualified_name
// use Bar; namespace_use_declaration > namespace_use_clause > name
// use Foo\Bar as Baz; the same, plus an `alias:` field we deliberately ignore — the DEPENDENCY is
// the real name, not the local nickname (mirroring csharpUsingTarget, which
// also reports the aliased target rather than the alias)
// the real name, not the local nickname
// use function Foo\bar; a `type:` field on the CLAUSE ("function"/"const"); the specifier is
// unchanged, so nothing here needs to branch on it
// use Foo\{A, B}; namespace_use_declaration > namespace_name + namespace_use_group — the
Expand Down Expand Up @@ -1632,9 +1603,8 @@ inline std::string phpUseTarget( TSNode useNode, std::string_view src )
}

// TS/JS `require("./x")` and dynamic `import("./x")` → the written specifier, or empty when this call
// expression is not a module load. Its own function for the same reason csharpUsingTarget and phpUseTarget
// are: one AST shape per language stays a one-line call at the use site rather than one branch plus one nested
// scan. #358: its readers are now the DepDialect::Web normaliser (src/ingest_importcap.h — the captured
// expression is not a module load. Like phpUseTarget, one AST shape stays a one-line call at the use site
// rather than one branch plus a nested scan. #358: its readers are now the DepDialect::Web normaliser (src/ingest_importcap.h — the captured
// `require`/`import` call) and captureJsImportFacts (src/ingest_jsimports.h — the binding a destructured
// require makes); directiveTargetOf no longer reads it, because the walk no longer visits TS/JS.
//
Expand Down Expand Up @@ -2641,7 +2611,7 @@ constexpr std::uint16_t kMaxImportContainerDepth = 256;
// at all. Split out of captureIncludes so the walk that FINDS directives and the per-grammar table that
// READS them stay separately readable — the walk is one shape, this is one branch per grammar spelling.
//
// Node types confirmed per grammar: Swift import_declaration; Rust use_declaration + mod_item; C# using_directive; plus the kParserVer-81 four (bash
// Node types confirmed per grammar: Swift import_declaration; Rust use_declaration + mod_item; plus the kParserVer-81 four (bash
// `command`, lua `function_call`, ruby `call`, elixir `call`). LEVER-B B0: non-C imports capture the
// CLEAN written specifier via grammar child fields (module path / quoted specifier / use argument), not a
// sliced clause — the sound resolver input.
Expand Down Expand Up @@ -2799,10 +2769,6 @@ DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src
// landing. Keep the best-effort target for --uses / --deps back-compat.
target = importClauseTarget( n, src );
}
else if( kindIs( t, "using_directive" ) )
{ // C# `using Foo.Bar;` / `using static Foo;` / `using X = Foo.Bar;`
target = csharpUsingTarget( n, src ); // see csharpUsingTarget for the shape rationale
}
else if( kindIs( t, "namespace_use_declaration" ) )
{ // PHP `use Foo\Bar;` / `use Foo\Bar as Baz;` / `use function Foo\bar;` / `use Foo\{A, B};`
// Captured for --uses / --deps visibility, NEVER precise-resolved: PHP has no entry in
Expand Down
2 changes: 1 addition & 1 deletion src/ingest_sidecap.h
Original file line number Diff line number Diff line change
Expand Up @@ -1639,7 +1639,7 @@ void captureSideFacts( const LangEntry& le, std::uint32_t fileId, std::string_vi
#endif

// Elixir directives share the lexical tags context below (ElixirContext), and as of #358 so do the
// C-family, Go, Python and TypeScript/JavaScript ones: their `@import.path` captures are normalised and emitted
// C-family, C#, Go, Python and TypeScript/JavaScript ones: their `@import.path` captures are normalised and emitted
// by captureTagsFacts, and captureIncludes no longer reads a preproc_include / preproc_call /
// import_statement / export_statement / require() call at all. The walk is pure cost for those
// languages — it descends only allowlisted containers, and the tags query already reaches the same
Expand Down
Loading
Loading