diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cd9ce1b..f4a8ec38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e5b54b7f..850bd10c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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**). | diff --git a/prompts/help-wanted/visual-basic-language.md b/prompts/help-wanted/visual-basic-language.md index 776a13a62..cbdfd3f26 100644 --- a/prompts/help-wanted/visual-basic-language.md +++ b/prompts/help-wanted/visual-basic-language.md @@ -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` diff --git a/queries/csharp/tags.scm b/queries/csharp/tags.scm index 29827b727..610b3d3b0 100644 --- a/queries/csharp/tags.scm +++ b/queries/csharp/tags.scm @@ -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 diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 27a183325..cc46f38f8 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -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 diff --git a/src/ingest_importcap.h b/src/ingest_importcap.h index 31a5dcdbb..c7fee2c51 100644 --- a/src/ingest_importcap.h +++ b/src/ingest_importcap.h @@ -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. @@ -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 } } @@ -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; @@ -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 ); diff --git a/src/ingest_relations.h b/src/ingest_relations.h index 17a5cfc8a..7e5e675ed 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -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 @@ -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 @@ -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. // @@ -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. @@ -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 diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index b401bcf44..a3f1c2025 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -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 diff --git a/src/lintrules.h b/src/lintrules.h index ea992b4ec..2647cd65a 100644 --- a/src/lintrules.h +++ b/src/lintrules.h @@ -206,7 +206,7 @@ inline Lang langOfPath( std::string_view path ) noexcept // ingest.cpp's captureIncludes(): every language below has a node-type branch there (Cpp/C/ObjC's // preproc_include/#import, Python/TS/JS's import_statement(_from), Rust's use_declaration/mod_item, // Go/Swift/Java's import_declaration — Java shares that node-type SPELLING with Go/Swift so it is -// captured too, even though only best-effort resolved — and C#'s using_directive; PHP's +// captured too, even though only best-effort resolved — and C#'s using_directive (captured via @import.path since #358); PHP's // namespace_use_declaration joined them in the PHP/Lua port round). // // kParserVer 81 (the four-language import round) ADDED Bash/Ruby/Lua/Elixir. Each now has its own diff --git a/src/quality.h b/src/quality.h index 7470c7d61..d903649a1 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2337,7 +2337,9 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 36; // MUST equal ingest.cpp's kCacheVersion (gated); 36 = train 30 (above call-result-typing's and fcall-forward's 33, name-template-dispatch's 35, main's 34), 34 = train 29 (above lane arity-min's and call-result-typing's 33), 33 = train 29 lane arity-min: def record paramsMin (the train renumbers), 33 = lane call-result-typing: RawBind resultIndex + unwrapDepth, 33 = lane fcall-forward: ref `forwarded` byte, 35 = lane name-template-dispatch: RefRole Pattern 11, 32 = train 28: RefRole Subscript 9 + Inert 10, 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 212; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 214; // MUST equal ingest.cpp's kParserVer (gated) + // 214 = 2026-10-21 (#358 1a, PR #420: above main 206/207 and train 32a 212/213; + // see the kParserVer note) // 212 = 2026-10-21 (train 32a: above main 206/207, rust-receivers 202/203, ix1-slot // 204/205, l6-iface 210/211; #420 reserves 214/215; see the kParserVer note) // 206 = 2026-10-18 (#409, PR #414, renumbered again at merge: above main 196/197, diff --git a/test/childwalkscalecheck.sh b/test/childwalkscalecheck.sh index 692225986..c1acd9ea9 100755 --- a/test/childwalkscalecheck.sh +++ b/test/childwalkscalecheck.sh @@ -239,7 +239,7 @@ for n in ( 1000, 16000 ): # captureFields, the declarator walk — a reference_declarator `T & /*…*/ m` w( "fielddecl/n%d/big.cpp" % n, [ "struct T { int q; };", "struct S", "{", " T &" ] + [ C ] * n + [ " m;", "};" ] ) w( "fielddecl_off/n%d/big.cpp" % n, [ "struct T { int q; };", "struct S", "{", " T & m;" ] + [ C ] * n + [ "};" ] ) - # csharpUsingTarget — a using_directive; its control floods a method BODY (see the header: a class body + # normaliseCSharpImport — a using_directive; its control floods a method BODY (see the header: a class body # or the file root is an ancestor of every def, and the ancestor scan would read the flood too) w( "csusing/n%d/big.cs" % n, [ "using" ] + [ C ] * n + [ " System.Text;", "namespace A { class K { void M() {} } }" ] ) w( "csusing_off/n%d/big.cs" % n, [ "using System.Text;", "namespace A { class K { void M() {" ] + [ C ] * n + [ "} } }" ] ) @@ -519,7 +519,7 @@ pair(){ # $1 = label, $2 = walk fixture, $3 = control fixture, $4 = what the pai pair "(B13) captureMacroBodyCalls" macroparams macroparams_off "a preproc_params list 16000 children wide vs the identical flood outside the #define" pair "(B14) captureFields/type" fieldtype fieldtype_off "a field's qualified_identifier type 16000 children wide vs the identical flood after the field" pair "(B15) captureFields/declarator" fielddecl fielddecl_off "a field's reference_declarator 16000 children wide vs the identical flood after the field" -pair "(B16) csharpUsingTarget" csusing csusing_off "a using_directive 16000 children wide vs the identical flood inside a method body" +pair "(B16) normaliseCSharpImport" csusing csusing_off "a using_directive 16000 children wide vs the identical flood inside a method body" pair "(B17) phpUseTarget" phpuse phpuse_off "a namespace_use_declaration 16000 children wide vs the identical flood after it" pair "(B18) jsModuleLoadTarget" jsrequire jsrequire_off "require()'s arguments 16000 children wide vs the identical flood after the statement" pair "(B19) rubyNamespaceOnly" rubyns rubyns_off "a class body_statement 16000 children wide vs the identical flood after the class" diff --git a/test/csharpcheck.sh b/test/csharpcheck.sh index 1c55b5aa4..c9c23eb30 100755 --- a/test/csharpcheck.sh +++ b/test/csharpcheck.sh @@ -23,8 +23,8 @@ # - `class Greeter : IGreeter` (base_list) emits an extends use-site: --uses=IGreeter shows # role="extends" in_id="Greeter"; --lego=IGreeter lists Greeter as its one implementor. # (§P8 2026-07-28: the attribute was in=, renamed to in_id= — see src/main.cpp runUses.) -# - `using System;` / `using CsharpFix.Services;` are captured as imports (ingest.cpp:: -# captureIncludes' using_directive branch, NOT tags.scm — matches every other language): +# - `using System;` / `using CsharpFix.Services;` are captured as imports (the +# shared @import.path capture and CSharp normaliser): # --uses=System shows role="import" p="Program.cs:1"; --deps lists both targets # verbatim (System, CsharpFix.Services) — proving both the bare-identifier and the dotted # qualified_name using-directive shapes are captured. @@ -223,6 +223,12 @@ grep -q "MAIN_EDGE_GONE:True" "$TMP/mut_check" \ && ok "mutation: renamed Program.cs call site -> Main -> SayHello edge vanished (non-tautological)" \ || no "mutation: Main -> SayHello edge survived a renamed call site — the edge assertion is a tautology" +if python3 "$ROOT/test/csharpimportcap.py" "$BIN" "$ROOT" "$TMP"; then + ok "C# slice 1a capture, legacy aliases, negative controls and depth disclosure" +else + no "C# slice 1a capture arms failed" +fi + # ─── Summary ────────────────────────────────────────────────────────────────── echo if [ "$fail" -eq 0 ]; then diff --git a/test/csharpimportcap.py b/test/csharpimportcap.py new file mode 100644 index 000000000..cce1581a9 --- /dev/null +++ b/test/csharpimportcap.py @@ -0,0 +1,114 @@ +"""C# slice 1a arms, invoked by csharpcheck.sh. Every output comes from the supplied binary.""" +import pathlib +import shutil +import subprocess +import sys +import xml.etree.ElementTree as ET + +binary, root, scratch = map(pathlib.Path, sys.argv[1:4]) +assert sys.argv[4:] in ([], ['--legacy']), 'unknown C# test contract' +legacy = sys.argv[4:] == ['--legacy'] + + +def run(where, *args): + proc = subprocess.run([str(binary), str(where), *args], capture_output=True, text=True) + if proc.returncode != 0: + raise RuntimeError((args, proc.returncode, proc.stderr)) + return proc.stdout + + +def targets(where): + xml = ET.fromstring(run(where, '--deps', '--limit=100000', '--no-cache')) + assert xml.tag == 'deps' + return [(f.attrib['p'], [i.attrib['t'] for i in f.findall('inc')]) for f in xml.findall('f')] + + +fixture = root / 'test/csharpimportcapfix' +expected = ['System', 'Demo.Widget', 'Alias', 'GlobalAlias', 'Demo.Widget', 'Demo.Widget', 'Demo', + 'global::Demo.Widget', 'Generic', 'Pair', 'Items', 'Maybe', 'Ptr', 'Demo', 'Demo', + 'Demo /* middle */ . Widget', 'CommentAlias', 'Nested'] + + +def check_rows(where): + assert targets(where) == [('Imports.cs', expected)], targets(where) + + +check_rows(fixture) +print(' PASS C# C1: exact targets, directive order, duplicates, comments, static/global and complex aliases') +for name, line in [('System', 1), ('Alias', 3), ('GlobalAlias', 4), ('Generic', 9), ('Nested', 21)]: + xml = ET.fromstring(run(fixture, '--uses=' + name, '--no-cache')) + assert any(u.get('role') == 'import' and u.get('p') == 'Imports.cs:' + str(line) for u in xml.findall('u')), (name, line) +print(' PASS C# C2: Import use-sites stay at their directives, including global and nested aliases') + +# Known gap, slice 2b after the shared binds channel (2a): an alias still names itself in Include, +# and using/static do not narrow these three calls to the RHS class. This refactor must not fix it. +xml = ET.fromstring(run(fixture, '--callees=Run', '--no-cache')) +assert xml.get('graph_ambiguous') == '3', xml.attrib +assert [(s.get('n'), s.get('p')) for s in xml.findall('s')] == [('Ping', 'Types.cs:1'), ('Ping', 'Types.cs:2')] +print(' PASS C# C3 known-gap (2b): alias remains Alias; all three Ping calls retain both candidates') + +# Mutate real source and run the identical row checker. A changed alias and a missing directive +# must each fail it; checking the mutation itself is only the presence guard, never the verdict. +for name, old, new in [('rename', 'using Alias =', 'using Renamed ='), ('remove', 'using System;\n', '')]: + work = scratch / name + shutil.copytree(fixture, work) + source = work / 'Imports.cs' + original = source.read_text() + assert old in original + source.write_text(original.replace(old, new, 1)) + assert source.read_text() != original + try: + check_rows(work) + except AssertionError: + print(' PASS C# C4 control: identical row checker rejects actual ' + name + ' mutation') + else: + raise AssertionError('row checker accepted ' + name) + +cache = scratch / 'using.cache' +fresh = run(fixture, '--deps', '--no-cache') +cold = run(fixture, '--deps', '--cache=' + str(cache)) +warm = run(fixture, '--deps', '--cache=' + str(cache)) +assert fresh == cold == warm +print(' PASS C# C5: using fixture fresh/cold/warm dependency bytes identical') + +# Observable rows alone also pass on the old walk. Check the actual embedded query, not merely +# the new source beside an old binary. --legacy validates BASE's old contract before implementation. +captured = b'(using_directive) @import.path' in binary.read_bytes() +retired = 'csharpUsingTarget' not in (root / 'src/ingest_relations.h').read_text() +assert captured == (not legacy), 'C# capture mechanism does not match the requested BASE/HEAD contract' +if not legacy: + assert retired, 'retired C# extractor remains in HEAD source' +print(' PASS C# C7: ' + ('BASE still uses the old walk' if legacy else 'embedded @import.path; old extractor deleted')) + +# Shared-capture disclosure, as in Python #382: a nonempty import beyond the bound is partial; +# deep containers without an import no longer trigger the retired walk's disclosure. Edges do not widen. +for depth in [254, 255, 256, 257, 258]: + for mode in ['using', 'empty', 'top']: + work = scratch / ('depth-' + str(depth) + '-' + mode) + work.mkdir() + pairs, pp = divmod(depth, 2) + source = ('using Top.Level;\n' if mode == 'top' else '') + source += ''.join('namespace N%d {\n' % i for i in range(pairs)) + source += '#if FLAG\n' if pp else '' + source += 'using Deep.Target;\n' if mode == 'using' else '\n' + source += '#endif\n' if pp else '' + source += '}\n' * pairs + (work / 'Probe.cs').write_text(source) + expected_rows = [('Probe.cs', ['Top.Level'])] if mode == 'top' else ( + [('Probe.cs', ['Deep.Target'])] if mode == 'using' and depth <= 256 else []) + assert targets(work) == expected_rows, (depth, mode, targets(work)) + cache_file = scratch / (work.name + '.cache') + cold_stat = None + for cache_arg in ['--no-cache', '--cache=' + str(scratch / (work.name + '.cache')), + '--cache=' + str(scratch / (work.name + '.cache'))]: + skipped = ET.fromstring(run(work, '--skipped', cache_arg)).find('skipped') + assert skipped is not None + partial = depth > 256 and (legacy or mode == 'using') + assert int(skipped.get('extract_partial', '0')) == int(partial), (depth, mode, skipped.attrib) + if cache_arg != '--no-cache': + stat = cache_file.stat() + stamp = (stat.st_ino, stat.st_mtime_ns) + if cold_stat is not None: + assert (stamp != cold_stat) == partial, (depth, mode, cold_stat, stamp) + cold_stat = stamp +print(' PASS C# C6: 254/255/256/257/258 boundary, using/empty/top; cold/warm disclosure and cache eligibility') diff --git a/test/csharpimportcapfix/Imports.cs b/test/csharpimportcapfix/Imports.cs new file mode 100644 index 000000000..c2fd70a98 --- /dev/null +++ b/test/csharpimportcapfix/Imports.cs @@ -0,0 +1,41 @@ +using System; +using Demo.Widget; +using Alias = Demo.Widget; +global using GlobalAlias = Demo.Widget; +using static Demo.Widget; +global using static Demo.Widget; +global using Demo; +using global::Demo.Widget; +using Generic = Demo.Box; +using Pair = (int left, string right); +using Items = Demo.Widget[]; +using Maybe = Demo.Widget?; +using unsafe Ptr = int*; +using Demo; +using Demo; +using /* before */ Demo /* middle */ . Widget; +using /* before */ CommentAlias /* alias */ = Demo . Widget; + +namespace Outer +{ + using Nested = Demo.Widget; + public class Caller + { + public static void Run(bool chooseWidget) + { + Alias.Ping(); + if (chooseWidget) + { + Widget.Ping(); + } + else + { + Ping(); + } + } + public void Resource() + { + using var resource = new Disposable(); + } + } +} diff --git a/test/csharpimportcapfix/Types.cs b/test/csharpimportcapfix/Types.cs new file mode 100644 index 000000000..0c823c3b7 --- /dev/null +++ b/test/csharpimportcapfix/Types.cs @@ -0,0 +1,2 @@ +namespace Demo { public class Widget { public static void Ping() {} } public class Box {} } +namespace Other { public class Widget { public static void Ping() {} } } diff --git a/test/langidentity/manifest.tsv b/test/langidentity/manifest.tsv index 5218a6927..8d19fdcfa 100644 --- a/test/langidentity/manifest.tsv +++ b/test/langidentity/manifest.tsv @@ -28,6 +28,7 @@ rb test/rubyconstfix rb test/rubyrecvfix cs test/csharpfix cs test/csharpcondfix +cs test/csharpimportcapfix php test/phpfix lua test/luafix lua test/luarequirefix diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index e60bed005..f0f14347c 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -c4422a58e4e5431372e8b06423ab8791a0c73496499b838c3dd548825962d607 +6093f6e27e694801716d56755d0b336eab930d19c11e7c962f04495d0a465043 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index e6ec064df..38ba83daf 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,11 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-21, C# import captures (#358 1a, PR #420 by @LaimaWu), merged with main 320f8d8f (train 32a): RE-PINNED with UPDATE_GOLDEN=1 (hash 6093f6e27e…5043; re-derived once on the merged tree, unchanged: main's +# train-32a numbers live in comments only). +# Watched text moved: kParserVer 206 -> 214 (rich file tag 215), quality.h's kIngestParserVerMirror with it. 214 is above main 206 / 207 and the +# train-32a numbers 212 / 213 (below). Import-free deep containers cease to be partial. kCacheVersion stays 36 and kQSnapCacheScheme stays 22: no record +# format moved. The entries below are kept as written; their hashes are superseded by this one. # 2026-10-21, train 32a (member-names 781a22fb, rust-receivers dd378171 merged onto main f156be85): RE-DERIVED ONCE with # UPDATE_GOLDEN=1 on the merged tree (a667ffc55e…8a05 -> c4422a58e4…2d607). Watched text: kParserVer 206 -> 212 (rich file tag # 213; above main 206 / 207, lane rust-receivers 202 / 203, ix1-slot 204 / 205, l6-iface 210 / 211; #420 reserves 214 / 215) and