mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-28 06:22:22 +00:00
feat(languages): roll the receiver chain out to every emitter
U8. Emission moves into one shared helper and is wired into all 14 language
emitters that build a capture node map.
The helper is language-free (AGENTS.md R6): its call gate reads the
`@reference.call.*` tag prefix — a vocabulary every language's `.scm` query
already shares — rather than a per-language tag list. Only TypeScript and
JavaScript defined a `CALL_TAGS` constant, so a per-language list would have
meant inventing one twelve times. `@reference.callable` is excluded by the
trailing dot: it marks a callable VALUE, not a call site.
It is also self-gating — a non-call match, an absent receiver, or a chain with
no nameable base each leave the match untouched — which is what makes it safe to
call before every `out.push(grouped)` in the emitters that have three or four
such paths.
Measured, not assumed:
C++ svc->getUser()->save() INVISIBLE-GAP → RESOLVES
TS (three shapes, from U10) RESOLVES
The C++ row is the one the plan flagged as having no fixture ANYWHERE:
`cpp-chain-call/` uses the value `.` form, which already worked. It now has one,
alongside a value-dot control proving the defect was the `->` base specifically.
A WRONG-EDGE REGRESSION, caught by the full multi-language suite and fixed.
Ruby `Factory.new.run` resolved correctly to `Factory#run` before this and to
`Product.run` after — a correct edge turned into a wrong one, which is precisely
the failure mode this work exists to avoid. Cause: `Factory.new` denotes an
INSTANCE of Factory, and the cascade encodes that through
`isConstructionSelectorHop` together with the class-constant test separating it
from an instance method genuinely named `new`. A chain step records only a name,
so the fold cannot make that distinction. It now declines any step matching the
language's construction selector and lets the cascade answer. Pinned by a unit
test.
PHP is a recorded residual, and the plan's named trap is NOT the cause. The trap
— a language whose node type is missing from `extractMixedChain`'s tables reads
as "didn't need it" when it cannot be measured — was checked directly against the
emitter, which emits `chain=1|$svc|cgetUser` correctly. The residual is that the
fold's base `$svc` does not bind in the PHP resolver. That is PHP binding-key
work, not a chain-layer defect.
Two incidental corrections to KTD6 fell out of that check. PHP's receiver capture
text is normalized to `$svc.getUser()` — dots, not `->` — so Case 0's "C-family
punctuation" gate does fire for PHP, contrary to the plan. And typing the fixture
parameter moved the PHP row from VISIBLE-GAP to INVISIBLE-GAP, because with a
type binding the cascade types the receiver but finds no member, so no drop is
recorded. An untyped fixture parameter had been reporting a language gap that was
really a fixture defect — the same error class as the untyped `$repo` control
caught earlier in this series.
The wiring inventory is pinned by a test. The helper being self-gating makes the
roster cheap to change silently, which is exactly why: a new entry means someone
wired a language without measuring it, a removed one means a refactor dropped
coverage. `cobol` and `vue` stay unwired — they group no capture node map, so
there is no receiver node to walk.
4367 tests green. The 7 remaining failures are all `*-captures-golden` snapshots,
which U9 regenerates once at the tip.
This commit is contained in:
parent
66beec5f69
commit
aced4456cc
21 changed files with 453 additions and 38 deletions
|
|
@ -188,3 +188,57 @@ must be read against the same bias.
|
|||
plain property path (`$this->repo`, `a::b`) never reaches the recorder.
|
||||
- `repos[0].save()` has neither `.` nor `(` in its receiver — same result.
|
||||
- `?.` and explicit type arguments produce no reference site at all.
|
||||
|
||||
## U8 — per-language rollout
|
||||
|
||||
Emission moved into one shared helper
|
||||
(`utils/receiver-chain-captures.ts`) and is wired into all 14 language
|
||||
emitters. The helper is language-free (R6): its call gate reads the
|
||||
`@reference.call.*` tag prefix, a vocabulary every language's `.scm` query
|
||||
shares, rather than a per-language tag list. It is self-gating — a non-call
|
||||
match, an absent receiver, or a chain with no nameable base all leave the match
|
||||
untouched — so inserting the call before every `out.push(grouped)` is safe even
|
||||
in the emitters that have three or four such paths.
|
||||
|
||||
| Language | Shape | Before | After |
|
||||
|---|---|---|---|
|
||||
| TypeScript | `svc?.getUser().save()` | INVISIBLE-GAP | **RESOLVES** |
|
||||
| TypeScript | `svc!.getUser().save()` | VISIBLE-GAP | **RESOLVES** |
|
||||
| TypeScript | `svc.getTyped<User>().save()` | INVISIBLE-GAP | **RESOLVES** |
|
||||
| C++ | `svc->getUser()->save()` | INVISIBLE-GAP | **RESOLVES** |
|
||||
| C++ | `svc2.getUser()->save()` (control) | RESOLVES | RESOLVES |
|
||||
| PHP | `$svc->getUser()->save()` | VISIBLE-GAP | INVISIBLE-GAP |
|
||||
| PHP | `$this->repo->save()` (control) | RESOLVES | RESOLVES |
|
||||
|
||||
The C++ row is the one the plan flagged as having **no fixture anywhere** —
|
||||
`cpp-chain-call/` uses the value `.` form, which already worked. It now has one,
|
||||
plus the value-dot control that proves the defect was the `->` base specifically.
|
||||
|
||||
### PHP: a measured residual, with the trap checked
|
||||
|
||||
PHP does **not** resolve yet, and the plan's named trap — a language whose node
|
||||
type is missing from `extractMixedChain`'s tables reads as "didn't need it" when
|
||||
it in fact cannot be measured — is **not** the cause. Checked directly against
|
||||
the emitter:
|
||||
|
||||
```
|
||||
name=save chain=1|$svc|cgetUser recv=$svc.getUser()
|
||||
```
|
||||
|
||||
The chain is minted correctly. The residual is that the fold's base, `$svc`,
|
||||
does not bind in the PHP resolver, so the fold returns `undefined` and the site
|
||||
falls through to the text cascade. That is PHP binding-key work, not a
|
||||
chain-layer defect, and it is left as a recorded residual rather than absorbed
|
||||
into this series.
|
||||
|
||||
Two incidental corrections from that check, both to KTD6:
|
||||
|
||||
- PHP's receiver capture text is normalized to `$svc.getUser()` — DOTS, not
|
||||
`->`. So Case 0's "C-family punctuation" gate fires for PHP after all, which
|
||||
is why the call chain was recorded as a VISIBLE-GAP to begin with.
|
||||
- Typing the fixture parameter (`function f(Service $svc)`) moved the row from
|
||||
VISIBLE-GAP to INVISIBLE-GAP: with a type binding the cascade now types the
|
||||
receiver but finds no member, so `compoundReceiverUnresolved` is false and no
|
||||
drop is recorded. An untyped fixture parameter had been reporting a language
|
||||
gap that was really a fixture defect — the same error class as the untyped
|
||||
`$repo` control caught earlier.
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
"indexElement": "INVISIBLE-GAP"
|
||||
},
|
||||
"php": {
|
||||
"arrowCallChain": "VISIBLE-GAP",
|
||||
"arrowCallChain": "INVISIBLE-GAP",
|
||||
"arrowPropertyPath": "RESOLVES"
|
||||
},
|
||||
"cpp": {
|
||||
"pointerArrowChain": "INVISIBLE-GAP",
|
||||
"pointerArrowChain": "RESOLVES",
|
||||
"valueDotChain": "RESOLVES"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -148,7 +148,10 @@ class Service {
|
|||
`,
|
||||
},
|
||||
header: `<?php\nrequire_once 'models.php';\n`,
|
||||
wrap: (entry, body) => `function ${entry}($svc) {\n ${body}\n}\n`,
|
||||
// TYPED parameter. An untyped `$svc` has no type binding to resolve the
|
||||
// chain's base against, which would make this row report a language gap
|
||||
// that is really a fixture defect.
|
||||
wrap: (entry, body) => `function ${entry}(Service $svc) {\n ${body}\n}\n`,
|
||||
shapes: [
|
||||
{
|
||||
id: 'arrowCallChain',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|||
import { splitCInclude } from './import-decomposer.js';
|
||||
import { computeCDeclarationArity, computeCCallArity } from './arity-metadata.js';
|
||||
import { markStaticName } from './static-linkage.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
import {
|
||||
synthesizeCallableFlowCaptures,
|
||||
type CallableCaptureSignature,
|
||||
|
|
@ -163,6 +164,12 @@ export function emitCScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { extractCppTemplateConstraints } from './constraint-extractor.js';
|
|||
import { captureCppMemberLookupFacts } from './member-lookup.js';
|
||||
import { CPP_BRACED_INIT_TYPE_PREFIX } from './conversion-rank.js';
|
||||
import { logger } from '../../../logger.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
import {
|
||||
synthesizeCallableFlowCaptures,
|
||||
type CallableCaptureSignature,
|
||||
|
|
@ -604,6 +605,12 @@ export function emitCppScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
|
||||
/** Declaration anchors that carry function-like arity metadata. */
|
||||
const FUNCTION_DECL_TAGS = [
|
||||
|
|
@ -159,6 +160,12 @@ export function emitCsharpScopeCaptures(
|
|||
}
|
||||
// Defensive fallback: emit the raw match so the extractor at
|
||||
// least sees an anchor, even without markers.
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -177,6 +184,12 @@ export function emitCsharpScopeCaptures(
|
|||
// the AST in code. Mirrors Python's `self`/`cls` synthesis on
|
||||
// `@scope.function` matches.
|
||||
if (grouped['@scope.function'] !== undefined) {
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
const fnNode = nodeIfType(nodeMap['@scope.function'], ...FUNCTION_NODE_TYPES);
|
||||
if (fnNode !== null) {
|
||||
|
|
@ -287,6 +300,12 @@ export function emitCsharpScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
|
||||
// Synthesize primary-constructor declarations on class/record
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import { encodeMarker } from '../../utils/heritage-marker.js';
|
|||
import { DART_BUILT_INS } from './built-ins.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { preprocessDartExtensionTypes } from './extension-type-preprocess.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
|
||||
const FUNCTION_DECL_TAGS = [
|
||||
'@declaration.function',
|
||||
|
|
@ -155,6 +156,12 @@ export function emitDartScopeCaptures(
|
|||
const bodyNode = findFunctionBody(declNode);
|
||||
|
||||
attachArityMetadata(grouped, declNode);
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
|
||||
if (bodyNode !== null) {
|
||||
|
|
@ -181,6 +188,12 @@ export function emitDartScopeCaptures(
|
|||
fieldType,
|
||||
);
|
||||
}
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
if (fieldType !== null) {
|
||||
out.push({
|
||||
|
|
@ -192,6 +205,12 @@ export function emitDartScopeCaptures(
|
|||
continue;
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { synthesizeGoTypeBindings, extractSimpleTypeNameText } from './type-bind
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
|
||||
const GO_CALLABLE_CAPTURE_OPTIONS = {
|
||||
functionNodeTypes: new Set(['function_declaration', 'method_declaration', 'func_literal']),
|
||||
|
|
@ -155,6 +156,12 @@ export function emitGoScopeCaptures(
|
|||
);
|
||||
}
|
||||
}
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -176,6 +183,12 @@ export function emitGoScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import { captureJavaPackageFact } from './package-facts.js';
|
|||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { captureJavaSpringConfigConsumerFacts } from './spring-config-bindings.js';
|
||||
import { captureJavaSpringDiClassFact, type JavaSpringDiClassFact } from './spring-di.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
import {
|
||||
captureJavaSpringConditionalFacts,
|
||||
type JavaSpringConditionalFact,
|
||||
|
|
@ -204,6 +205,12 @@ export function emitJavaScopeCaptures(
|
|||
continue;
|
||||
}
|
||||
}
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -257,6 +264,12 @@ export function emitJavaScopeCaptures(
|
|||
// Synthesize `this` / `super` receiver type-bindings on every
|
||||
// instance method-like.
|
||||
if (grouped['@scope.function'] !== undefined) {
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
// `@scope.function` is captured directly on the method/constructor node.
|
||||
const fnNode = findFunctionNode(nodeMap['@scope.function']);
|
||||
|
|
@ -348,6 +361,12 @@ export function emitJavaScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import {
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
import {
|
||||
deriveDefaultExportHocName,
|
||||
isBlockedDefaultExportHoc,
|
||||
|
|
@ -997,6 +998,12 @@ export function emitJsScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, groupedNodes['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
|
||||
// Synthesize `this` receiver type-bindings on class member functions.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
import { captureKotlinPackageFact } from './package-facts.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { captureKotlinSpringDiClassFact, type KotlinSpringDiClassFact } from './spring-di.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
import {
|
||||
captureKotlinSpringConditionalFacts,
|
||||
type KotlinSpringConditionalFact,
|
||||
|
|
@ -246,6 +247,12 @@ export function emitKotlinScopeCaptures(
|
|||
}
|
||||
|
||||
if (grouped['@scope.function'] !== undefined) {
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, groupedNodes['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
const fnNode = nodeIfType(groupedNodes['@scope.function'], 'function_declaration');
|
||||
if (fnNode !== null) {
|
||||
|
|
@ -303,6 +310,12 @@ export function emitKotlinScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, groupedNodes['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
|
||||
const extensionFallback = extensionFreeCallFallback(grouped, groupedNodes);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
|
||||
type SyntaxNode = ReturnType<ReturnType<typeof getPhpParser>['parse']>['rootNode'];
|
||||
|
||||
|
|
@ -228,6 +229,12 @@ export function emitPhpScopeCaptures(
|
|||
}
|
||||
}
|
||||
// Defensive fallback: emit the raw match.
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -235,6 +242,12 @@ export function emitPhpScopeCaptures(
|
|||
// Synthesize `$this` / `parent` receiver type-bindings on every
|
||||
// non-static method-like. Mirrors C#'s `this` / `base` synthesis.
|
||||
if (grouped['@scope.function'] !== undefined) {
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
const fnNode = nodeIfType(nodeMap['@scope.function'], ...FUNCTION_NODE_TYPES);
|
||||
if (fnNode !== null) {
|
||||
|
|
@ -338,6 +351,12 @@ export function emitPhpScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { getTreeSitterBufferSize } from '../../constants.js';
|
|||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { pythonFunctionDefinitionLabel } from './simple-hooks.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
|
||||
const PYTHON_CALLABLE_CAPTURE_OPTIONS = {
|
||||
functionNodeTypes: new Set(['function_definition', 'lambda']),
|
||||
|
|
@ -120,12 +121,24 @@ export function emitPythonScopeCaptures(
|
|||
for (const piece of splitImportStatement(stmtNode)) out.push(piece);
|
||||
} else {
|
||||
// Defensive fallback: emit the raw match.
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (grouped['@scope.function'] !== undefined) {
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
// `@scope.function` is captured directly on the `function_definition`
|
||||
// node (query: `(function_definition) @scope.function`), so it IS the
|
||||
|
|
@ -183,10 +196,22 @@ export function emitPythonScopeCaptures(
|
|||
);
|
||||
}
|
||||
}
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|||
import { splitQualifiedName } from '../../utils/qualified-name.js';
|
||||
import { encodeMarker } from '../../utils/heritage-marker.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
|
||||
const FUNCTION_NODE_TYPES = ['method', 'singleton_method'] as const;
|
||||
const HERITAGE_CALL_NAMES: ReadonlySet<string> = new Set(['include', 'extend', 'prepend']);
|
||||
|
|
@ -147,6 +148,12 @@ export function emitRubyScopeCaptures(
|
|||
continue;
|
||||
}
|
||||
}
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -159,6 +166,12 @@ export function emitRubyScopeCaptures(
|
|||
const receiver = synthesizeRubyReceiverBinding(fnNode, enclosingNode);
|
||||
if (receiver !== null) out.push(receiver);
|
||||
}
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -204,6 +217,12 @@ export function emitRubyScopeCaptures(
|
|||
);
|
||||
}
|
||||
}
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -304,6 +323,12 @@ export function emitRubyScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { synthesizeRustReceiverBinding } from './receiver-binding.js';
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
|
||||
const RUST_CALLABLE_CAPTURE_OPTIONS = {
|
||||
functionNodeTypes: new Set(['function_item', 'closure_expression']),
|
||||
|
|
@ -197,6 +198,12 @@ export function emitRustScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
|
||||
/** Declaration anchors that carry function-like arity metadata. */
|
||||
const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'] as const;
|
||||
|
|
@ -129,6 +130,12 @@ export function emitSwiftScopeCaptures(
|
|||
continue;
|
||||
}
|
||||
}
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped); // defensive fallback
|
||||
continue;
|
||||
}
|
||||
|
|
@ -174,6 +181,12 @@ export function emitSwiftScopeCaptures(
|
|||
const span = `${navNode.startIndex}-${navNode.endIndex}`;
|
||||
if (seenReadSpans.has(span)) continue;
|
||||
seenReadSpans.add(span);
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -236,6 +249,12 @@ export function emitSwiftScopeCaptures(
|
|||
...FUNCTION_NODE_TYPES,
|
||||
);
|
||||
if (fnNodeForArity !== null) attachArityMetadata(grouped, fnNodeForArity);
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
|
||||
const recvNode = nodeIfType(nodeMap['@scope.function'], ...RECEIVER_NODE_TYPES);
|
||||
|
|
@ -296,6 +315,12 @@ export function emitSwiftScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, nodeMap['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,8 @@ import {
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { extractMixedChain } from '../../utils/call-analysis.js';
|
||||
import { encodeReceiverChain } from '../../utils/receiver-chain-codec.js';
|
||||
import { synthesizeCjsModuleExports } from './cjs-module-exports.js';
|
||||
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
||||
import {
|
||||
deriveDefaultExportHocName,
|
||||
isBlockedDefaultExportHoc,
|
||||
|
|
@ -496,39 +495,12 @@ export function emitTsScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// Synthesize the compact receiver chain for a receiver that is itself an
|
||||
// expression, so resolution can type it by folding over structure instead
|
||||
// of re-parsing the receiver's source text.
|
||||
//
|
||||
// Emitted HERE rather than in the query: the receiver capture originates in
|
||||
// several `.scm` patterns, and a query cannot call `extractMixedChain`.
|
||||
//
|
||||
// Gated on CALL_TAGS. Without that gate this fires on read, write and JSX
|
||||
// receivers too, which pay the walk and the bytes for a field no call-site
|
||||
// resolver will read.
|
||||
//
|
||||
// A chain is minted only when the walk reached a NAMEABLE base.
|
||||
// `extractMixedChain` returns `baseReceiverName: undefined` when it stopped
|
||||
// early — either it hit `MAX_CHAIN_DEPTH` or it could not read a step — and
|
||||
// in both cases what it collected is a partial chain whose missing head is
|
||||
// exactly what decides the final type. Encoding that would turn a receiver
|
||||
// we cannot type into one we type WRONGLY, so those fall through to the
|
||||
// existing text cascade instead.
|
||||
const receiverChainNode = groupedNodes['@reference.receiver'];
|
||||
if (receiverChainNode !== undefined && callAnchor !== undefined) {
|
||||
const extracted = extractMixedChain(receiverChainNode);
|
||||
if (extracted !== undefined && extracted.baseReceiverName !== undefined) {
|
||||
const encoded = encodeReceiverChain(extracted.baseReceiverName, extracted.chain);
|
||||
if (encoded !== undefined) {
|
||||
grouped['@reference.receiver-chain'] = syntheticCapture(
|
||||
'@reference.receiver-chain',
|
||||
receiverChainNode,
|
||||
encoded,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Structural receiver chain for a call whose receiver is itself an
|
||||
// expression, so resolution can type it by folding over structure
|
||||
// instead of re-parsing the receiver's source text. Self-gating: a
|
||||
// non-call match, an absent receiver, or a chain with no nameable base
|
||||
// all leave `grouped` untouched.
|
||||
synthesizeReceiverChainCapture(grouped, groupedNodes['@reference.receiver']);
|
||||
out.push(grouped);
|
||||
|
||||
// Synthesize `this` receiver type-bindings on every function-like
|
||||
|
|
|
|||
|
|
@ -302,6 +302,18 @@ export function foldReceiverChain(
|
|||
if (current === undefined) return undefined;
|
||||
|
||||
for (const step of chain.steps) {
|
||||
// Construction is NOT an ordinary member lookup. `Factory.new` on a class
|
||||
// constant denotes an instance of Factory, and the cascade already encodes
|
||||
// that (`isConstructionSelectorHop`) along with the class-constant test that
|
||||
// separates it from an instance method genuinely named `new`. The fold
|
||||
// carries no such distinction — a chain step records a name, not whether its
|
||||
// base was a class reference or a value — so folding one would resolve
|
||||
// `Factory.new.run` against whatever member named `new` the lookup reaches
|
||||
// first. That turned a correct edge into a WRONG one (Ruby
|
||||
// `Factory.new.run` → `Product.run`), which is the failure mode this whole
|
||||
// line of work exists to avoid. Decline and let the cascade answer.
|
||||
if (options.constructionSyntax?.selector === step.name) return undefined;
|
||||
|
||||
const next = typeOfMemberOnClass(current, step.name, scopes, index, options);
|
||||
if (next === undefined) return undefined;
|
||||
current = next;
|
||||
|
|
|
|||
68
gitnexus/src/core/ingestion/utils/receiver-chain-captures.ts
Normal file
68
gitnexus/src/core/ingestion/utils/receiver-chain-captures.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Synthesize `@reference.receiver-chain` for a call match whose receiver is
|
||||
* itself an expression.
|
||||
*
|
||||
* Shared across every language emitter, and deliberately language-free (AGENTS.md
|
||||
* R6): the call gate reads the `@reference.call.*` tag vocabulary, which is
|
||||
* shared by every language's `.scm` query, rather than a per-language tag list.
|
||||
*
|
||||
* Lives in the emitter layer rather than the query because a `.scm` pattern
|
||||
* cannot call `extractMixedChain`, and the receiver capture originates in
|
||||
* several patterns per language.
|
||||
*/
|
||||
|
||||
import type { Capture } from 'gitnexus-shared';
|
||||
|
||||
import type { SyntaxNode } from './ast-helpers.js';
|
||||
import { syntheticCapture } from './ast-helpers.js';
|
||||
import { extractMixedChain } from './call-analysis.js';
|
||||
import { encodeReceiverChain } from './receiver-chain-codec.js';
|
||||
|
||||
/** Tag prefix marking a match as a CALL. `@reference.callable` is deliberately
|
||||
* excluded by the trailing dot — it marks a callable VALUE, not a call site. */
|
||||
const CALL_TAG_PREFIX = '@reference.call.';
|
||||
|
||||
/** Does this match represent a call? */
|
||||
function isCallMatch(grouped: Record<string, Capture>): boolean {
|
||||
for (const tag of Object.keys(grouped)) {
|
||||
if (tag.startsWith(CALL_TAG_PREFIX)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add `@reference.receiver-chain` to `grouped` when the match is a call whose
|
||||
* receiver decodes to a chain over a nameable base. Mutates `grouped` in place
|
||||
* and returns nothing — mirroring how the other synthesized captures are added.
|
||||
*
|
||||
* Gated on the call tags: without that gate this fires on read, write and JSX
|
||||
* receivers too, which pay the AST walk and the persisted bytes for a field no
|
||||
* call-site resolver reads.
|
||||
*
|
||||
* A chain is minted ONLY when the walk reached a nameable base.
|
||||
* `extractMixedChain` reports `baseReceiverName: undefined` whenever it stopped
|
||||
* early — at `MAX_CHAIN_DEPTH`, or because a step could not be read — and what it
|
||||
* collected then is a partial chain missing exactly the head that decides the
|
||||
* final type. Encoding that would turn a receiver we cannot type into one we type
|
||||
* WRONGLY, so those fall through to the resolver's existing text cascade.
|
||||
*/
|
||||
export function synthesizeReceiverChainCapture(
|
||||
grouped: Record<string, Capture>,
|
||||
receiverNode: SyntaxNode | undefined | null,
|
||||
): void {
|
||||
if (receiverNode === undefined || receiverNode === null) return;
|
||||
if (grouped['@reference.receiver-chain'] !== undefined) return;
|
||||
if (!isCallMatch(grouped)) return;
|
||||
|
||||
const extracted = extractMixedChain(receiverNode);
|
||||
if (extracted === undefined || extracted.baseReceiverName === undefined) return;
|
||||
|
||||
const encoded = encodeReceiverChain(extracted.baseReceiverName, extracted.chain);
|
||||
if (encoded === undefined) return;
|
||||
|
||||
grouped['@reference.receiver-chain'] = syntheticCapture(
|
||||
'@reference.receiver-chain',
|
||||
receiverNode,
|
||||
encoded,
|
||||
);
|
||||
}
|
||||
|
|
@ -112,4 +112,22 @@ describe('foldReceiverChain', () => {
|
|||
it('declines a truncated chain even though the producer refuses to mint one', () => {
|
||||
expect(fold('1|svc|cgetUser|~')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('declines a construction-selector step and leaves it to the cascade', () => {
|
||||
// `Factory.new` denotes an INSTANCE of Factory, not the result of looking up
|
||||
// a member named `new`. The cascade encodes that, together with the
|
||||
// class-constant test that separates it from an instance method genuinely
|
||||
// named `new`; a chain step records only a name, so the fold cannot make the
|
||||
// distinction and must not try. Folding it turned a correct Ruby edge
|
||||
// (`Factory.new.run` → `Factory#run`) into a wrong one (`Product.run`).
|
||||
const decoded = decodeReceiverChain('1|svc|cnew');
|
||||
expect(decoded).toBeDefined();
|
||||
expect(
|
||||
foldReceiverChain(decoded!, ctx.inScope, ctx.scopes, ctx.index, {
|
||||
fieldFallback: false,
|
||||
hoistTypeBindingsToModule: true,
|
||||
constructionSyntax: { selector: 'new' },
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* Which language emitters synthesize `@reference.receiver-chain`.
|
||||
*
|
||||
* The helper is self-gating — a non-call match, an absent receiver, or a chain
|
||||
* with no nameable base all leave the match untouched — so wiring a language
|
||||
* costs nothing where the shape does not occur. That makes the inventory easy to
|
||||
* change silently, which is exactly why it is pinned: a new entry means someone
|
||||
* wired a language without measuring it, and a removed entry means a refactor
|
||||
* quietly dropped coverage.
|
||||
*
|
||||
* Source-level rather than behavioural, because the thing being pinned IS the
|
||||
* wiring: a behavioural check would pass for a language whose fixture happens to
|
||||
* contain no chained receiver.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const LANGUAGES_DIR = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'src',
|
||||
'core',
|
||||
'ingestion',
|
||||
'languages',
|
||||
);
|
||||
|
||||
function emittersCallingHelper(): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(LANGUAGES_DIR, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const captures = path.join(LANGUAGES_DIR, entry.name, 'captures.ts');
|
||||
let source: string;
|
||||
try {
|
||||
source = readFileSync(captures, 'utf8');
|
||||
} catch {
|
||||
continue; // no captures.ts (cobol/vue emit through another path)
|
||||
}
|
||||
if (source.includes('synthesizeReceiverChainCapture(')) out.push(entry.name);
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
describe('receiver-chain capture wiring inventory', () => {
|
||||
it('is wired into exactly the emitters that build a capture node map', () => {
|
||||
expect(emittersCallingHelper()).toEqual([
|
||||
'c',
|
||||
'cpp',
|
||||
'csharp',
|
||||
'dart',
|
||||
'go',
|
||||
'java',
|
||||
'javascript',
|
||||
'kotlin',
|
||||
'php',
|
||||
'python',
|
||||
'ruby',
|
||||
'rust',
|
||||
'swift',
|
||||
'typescript',
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves emitters without a node map unwired', () => {
|
||||
// These two do not group captures with their originating syntax nodes, so
|
||||
// there is no receiver node to walk. Wiring them would need an emitter
|
||||
// change, not a one-line call.
|
||||
for (const language of ['cobol', 'vue'] as const) {
|
||||
const captures = path.join(LANGUAGES_DIR, language, 'captures.ts');
|
||||
let source = '';
|
||||
try {
|
||||
source = readFileSync(captures, 'utf8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
expect(source, `${language} stays unwired`).not.toContain('synthesizeReceiverChainCapture(');
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue