fix(java): capture enum interface heritage (#2935)

* fix(java): capture enum interface heritage

* fix(java): harden enum heritage dispatch

* test(java): refresh synthetic capture baselines

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
azizur100389 2026-08-13 07:04:00 +01:00 committed by GitHub
parent 25e51eac96
commit cdc98a9cf8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 448 additions and 58 deletions

View file

@ -107,6 +107,10 @@ export interface SymbolDefinition {
* Unavailable callables still participate in overload selection, but a * Unavailable callables still participate in overload selection, but a
* selected unavailable target must suppress edge emission. */ * selected unavailable target must suppress edge emission. */
isDeleted?: boolean; isDeleted?: boolean;
/** True when the declaration identity was synthesized rather than written in
* source (for example an anonymous class). Consumers may use this only as a
* conservative priority hint; it does not change graph-node identity. */
isSynthetic?: boolean;
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */ /** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
ownerId?: string; ownerId?: string;
/** #1982/#1993: bridge-held enclosing-namespace path (e.g. `NS1`, `Outer.Inner`) /** #1982/#1993: bridge-held enclosing-namespace path (e.g. `NS1`, `Outer.Inner`)

View file

@ -133,8 +133,9 @@
"_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0." "_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0."
}, },
"java": { "java": {
"fingerprint": "36d689c58526c4482fbd701d1d9ca156623a3970734ead145717858712271ab5", "fingerprint": "2e2150b4f4d64519e3f4c6d7a2c12259178d3117872203c904fab8cba96a694a",
"scaling_budget": 1.5, "scaling_budget": 1.5,
"_rebaselined_2935_synthetic_declarations": "PR #2935 review follow-up: synthesized Java anonymous classes and bodied enum constants now carry the presence-only @declaration.is-synthetic sidecar used to preserve source-written dispatch targets at the fanout cap. DIGEST DRIFT ONLY, NOT A CAPTURE-SET CHANGE: the tag is attached to existing synthetic declaration matches; capture groups and fixture count remain 5755/18405, 3512, and 206. Prior 36d689c58526c4482fbd701d1d9ca156623a3970734ead145717858712271ab5 -> 2e2150b4f4d64519e3f4c6d7a2c12259178d3117872203c904fab8cba96a694a; CI scaling 0.971 < 1.5.",
"_rebaselined_2900_record_heritage": "#2900 review follow-up: the Java scale unit now includes a record implementing Marker, so the record-declaration @reference.inherits path is fingerprinted and exercised at scale. Prior b29e263524f55151dcb7cfc4c929d3d1d7bb360355cee4e832158f927857f663 -> 36d689c58526c4482fbd701d1d9ca156623a3970734ead145717858712271ab5; scaling 1.042 < 1.5.", "_rebaselined_2900_record_heritage": "#2900 review follow-up: the Java scale unit now includes a record implementing Marker, so the record-declaration @reference.inherits path is fingerprinted and exercised at scale. Prior b29e263524f55151dcb7cfc4c929d3d1d7bb360355cee4e832158f927857f663 -> 36d689c58526c4482fbd701d1d9ca156623a3970734ead145717858712271ab5; scaling 1.042 < 1.5.",
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata; same-name lexical regions use an O(ancestor-depth) ID-set lookup. Prior d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a -> 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4; scaling 0.992 < 1.5.", "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata; same-name lexical regions use an O(ancestor-depth) ID-set lookup. Prior d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a -> 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4; scaling 0.992 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Java method-reference/SAM callable flow facts with invocation-result suppression. Prior 062d754764aaa8a6772fb90875c710502a63e3e7a300e633942381ed914faada -> d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a; scaling 1.074 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Java method-reference/SAM callable flow facts with invocation-result suppression. Prior 062d754764aaa8a6772fb90875c710502a63e3e7a300e633942381ed914faada -> d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a; scaling 1.074 < 1.5.",
@ -154,8 +155,9 @@
"fixture_count": 206 "fixture_count": 206
}, },
"java-local-types": { "java-local-types": {
"fingerprint": "8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633", "fingerprint": "560734cd053fb4f4b23aa04bc7870c22089a8deedb0217fa9c1b4db689e02a97",
"scaling_budget": 1.5, "scaling_budget": 1.5,
"_rebaselined_2935_synthetic_declarations": "PR #2935 review follow-up: the local-type stress corpus includes synthesized anonymous declarations, which now carry the presence-only @declaration.is-synthetic sidecar. DIGEST DRIFT ONLY, NOT A CAPTURE-SET CHANGE. Prior 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633 -> 560734cd053fb4f4b23aa04bc7870c22089a8deedb0217fa9c1b4db689e02a97; CI scaling 1.002 < 1.5.",
"_added": "#2562 performance follow-up: co-scales same-host, same-name local classes and anonymous classes to gate JLS binary-name ordinal allocation. Precomputed per-sequence ordinals reduce the focused 100->800 workload from 176->6655ms to 141->752ms; normalized 250->800 scaling is 1.054.", "_added": "#2562 performance follow-up: co-scales same-host, same-name local classes and anonymous classes to gate JLS binary-name ordinal allocation. Precomputed per-sequence ordinals reduce the focused 100->800 workload from 176->6655ms to 141->752ms; normalized 250->800 scaling is 1.054.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior a9ad88de21ca6747a923260dbdf677fb74a004abbf9d57781f745e3a9027530b -> 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236.", "_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior a9ad88de21ca6747a923260dbdf677fb74a004abbf9d57781f745e3a9027530b -> 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236 -> 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633." "_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 3ca67847ea2b9a71b0a41e09f943767e5a2d3a113d3e203499ee364e37f40236 -> 8c50bbc83dff4f7f5abd06078aa6abc6b64af05fddb17ee826b5f3df3d346633."

View file

@ -17,3 +17,10 @@ export const SPRING_CONFIG_BINDINGS_FEATURE: AnalysisFeatureDescriptor = {
(filePath) => filePath.toLowerCase().endsWith('.java') || isSpringApplicationConfig(filePath), (filePath) => filePath.toLowerCase().endsWith('.java') || isSpringApplicationConfig(filePath),
), ),
}; };
/** Durable completeness contract for Java heritage captures. */
export const JAVA_ENUM_INTERFACE_HERITAGE_FEATURE: AnalysisFeatureDescriptor = {
id: 'java.heritage-captures',
version: 1,
appliesTo: (filePaths) => filePaths.some((filePath) => filePath.toLowerCase().endsWith('.java')),
};

View file

@ -424,6 +424,7 @@ function synthesizeJavaAnonymousClassDeclarations(rootNode: SyntaxNode): Capture
out.push({ out.push({
'@declaration.class': nodeToCapture('@declaration.class', body), '@declaration.class': nodeToCapture('@declaration.class', body),
'@declaration.name': syntheticCapture('@declaration.name', body, identity.name), '@declaration.name': syntheticCapture('@declaration.name', body, identity.name),
'@declaration.is-synthetic': syntheticCapture('@declaration.is-synthetic', body, 'true'),
}); });
// Inheritance: the anonymous class extends/implements its constructed // Inheritance: the anonymous class extends/implements its constructed
@ -485,6 +486,11 @@ function synthesizeJavaAnonymousClassDeclarations(rootNode: SyntaxNode): Capture
out.push({ out.push({
'@declaration.class': nodeToCapture('@declaration.class', bodyNode), '@declaration.class': nodeToCapture('@declaration.class', bodyNode),
'@declaration.name': syntheticCapture('@declaration.name', bodyNode, bodiedIdentity.name), '@declaration.name': syntheticCapture('@declaration.name', bodyNode, bodiedIdentity.name),
'@declaration.is-synthetic': syntheticCapture(
'@declaration.is-synthetic',
bodyNode,
'true',
),
}); });
if (hostEnum !== undefined) { if (hostEnum !== undefined) {
out.push({ out.push({
@ -636,12 +642,14 @@ function findEnclosingTypeDeclaration(node: SyntaxNode): SyntaxNode | null {
* `emitCppInheritanceCaptures`). * `emitCppInheritanceCaptures`).
* *
* Scope covers `class_declaration` (`superclass` extends + `interfaces` * Scope covers `class_declaration` (`superclass` extends + `interfaces`
* implements clauses), `record_declaration` (`interfaces` implements clauses), * implements clauses), `record_declaration` and `enum_declaration`
* and `interface_declaration` (`extends_interfaces` clauses). Interface * (`interfaces` implements clauses), and `interface_declaration`
* (`extends_interfaces` clauses). Interface
* inheritance was restored for registry-primary resolution in #1951. Record * inheritance was restored for registry-primary resolution in #1951. Record
* graph nodes became canonical link targets in #2801 / PR #2871, so their * graph nodes became canonical link targets in #2801 / PR #2871, so their
* `implements` clauses must participate for interface dispatch (#2900). Java * `implements` clauses must participate for interface dispatch (#2900).
* enum interface heritage remains a separately tracked gap (#2918). * Enums use the same tree-sitter `interfaces` field and participate as
* class-like `Enum` graph nodes (#2918).
* *
* Generic bases (`extends Box<T>`, `implements IFoo<T>`) and qualified bases * Generic bases (`extends Box<T>`, `implements IFoo<T>`) and qualified bases
* (`a.b.Base`, `a.b.Box<T>`, `a.b.IFoo<T>`) are normalized to their simple * (`a.b.Base`, `a.b.Box<T>`, `a.b.IFoo<T>`) are normalized to their simple
@ -664,9 +672,13 @@ function synthesizeJavaInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
for (const base of superclass.namedChildren) emitJavaInheritanceBase(out, base); for (const base of superclass.namedChildren) emitJavaInheritanceBase(out, base);
} }
} }
if (node.type === 'class_declaration' || node.type === 'record_declaration') { if (
// Records cannot declare a superclass; they share only the class node.type === 'class_declaration' ||
// `interfaces` arm. node.type === 'record_declaration' ||
node.type === 'enum_declaration'
) {
// Records and enums cannot declare a superclass; all three declarations
// expose implemented interfaces through the same tree-sitter field.
const interfaces = node.childForFieldName('interfaces'); const interfaces = node.childForFieldName('interfaces');
if (interfaces !== null) { if (interfaces !== null) {
for (const typeList of interfaces.namedChildren) { for (const typeList of interfaces.namedChildren) {
@ -724,15 +736,22 @@ function javaBaseSimpleNameOf(typeNode: SyntaxNode): string | undefined {
function javaBaseLookupNameNode(node: SyntaxNode): SyntaxNode | null { function javaBaseLookupNameNode(node: SyntaxNode): SyntaxNode | null {
switch (node.type) { switch (node.type) {
case 'type_identifier': case 'type_identifier':
return node; return node.isMissing || node.text.length === 0 ? null : node;
case 'scoped_type_identifier': case 'scoped_type_identifier': {
// `java.io.Serializable` → trailing `type_identifier` (`Serializable`). // `java.io.Serializable` → trailing `type_identifier` (`Serializable`).
return node.lastNamedChild; const tail = node.lastNamedChild;
return tail === null ? null : javaBaseLookupNameNode(tail);
}
case 'generic_type': { case 'generic_type': {
// `Box<String>` → recurse into the base type (`Box`). // `Box<String>` → recurse into the base type (`Box`).
const first = node.firstNamedChild; const first = node.firstNamedChild;
return first === null ? null : javaBaseLookupNameNode(first); return first === null ? null : javaBaseLookupNameNode(first);
} }
case 'annotated_type': {
// The final named child is the base type; preceding children are annotations.
const type = node.lastNamedChild;
return type === null ? null : javaBaseLookupNameNode(type);
}
default: default:
return null; return null;
} }

View file

@ -701,6 +701,7 @@ function buildDefFromDeclarationMatch(
const typeParameters = parseTypeParameterList(match['@declaration.type-parameters']?.text ?? ''); const typeParameters = parseTypeParameterList(match['@declaration.type-parameters']?.text ?? '');
const isExplicit = parseBooleanCapture(match['@declaration.is-explicit']); const isExplicit = parseBooleanCapture(match['@declaration.is-explicit']);
const isDeleted = parseBooleanCapture(match['@declaration.is-deleted']); const isDeleted = parseBooleanCapture(match['@declaration.is-deleted']);
const isSynthetic = parseBooleanCapture(match['@declaration.is-synthetic']);
return { return {
nodeId: makeDefId(filePath, anchor.range, type, nameCap.text), nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
@ -718,6 +719,7 @@ function buildDefFromDeclarationMatch(
...(templateConstraints !== undefined ? { templateConstraints } : {}), ...(templateConstraints !== undefined ? { templateConstraints } : {}),
...(isExplicit === true ? { isExplicit: true } : {}), ...(isExplicit === true ? { isExplicit: true } : {}),
...(isDeleted === true ? { isDeleted: true } : {}), ...(isDeleted === true ? { isDeleted: true } : {}),
...(isSynthetic === true ? { isSynthetic: true } : {}),
}; };
} }

View file

@ -467,35 +467,85 @@ export function emitReceiverBoundCalls(
if (subtypesBySupertypeDefId.get(ownerDef.nodeId) === undefined) return 0; if (subtypesBySupertypeDefId.get(ownerDef.nodeId) === undefined) return 0;
// Collect concrete targets across the closure first, so the cap below counts // Collect concrete targets across the closure first, so the cap below counts
// real dispatch targets rather than types visited. // real dispatch targets rather than types visited. Source-written owners
const targets: SymbolDefinition[] = []; // rank ahead of synthesized owners so a large anonymous implementation
const seenTypes = new Set<string>([ownerDef.nodeId]); // family cannot consume the whole budget. Within each group, the priority
const queue: string[] = [ownerDef.nodeId]; // counts concrete implementations already encountered on the path: the
while (queue.length > 0) { // first implementation under an abstract branch ranks ahead of deeper
const superId = queue.shift() as string; // overrides. Carrying that count through this existing walk avoids a reverse
for (const subDef of subtypesBySupertypeDefId.get(superId) ?? []) { // traversal per target at every call site.
if (seenTypes.has(subDef.nodeId)) continue; type DispatchTarget = {
seenTypes.add(subDef.nodeId); readonly member: SymbolDefinition;
queue.push(subDef.nodeId); readonly syntheticOwnerPriority: number;
const implMember = pickOverload(subDef.nodeId, memberName, site, model, provider); readonly ancestorImplementationCount: number;
readonly discoveryOrder: number;
};
type DispatchTraversal = {
readonly typeId: string;
readonly ancestorImplementationCount: number;
};
const targetByMemberId = new Map<string, DispatchTarget>();
const bestIncomingCount = new Map<string, number>([[ownerDef.nodeId, 0]]);
const queue: DispatchTraversal[] = [
{ typeId: ownerDef.nodeId, ancestorImplementationCount: 0 },
];
let head = 0;
let discoveryOrder = 0;
while (head < queue.length) {
const current = queue[head++]!;
for (const subDef of subtypesBySupertypeDefId.get(current.typeId) ?? []) {
const previousIncomingCount = bestIncomingCount.get(subDef.nodeId);
if ( if (
implMember === undefined || previousIncomingCount !== undefined &&
implMember === OVERLOAD_AMBIGUOUS || previousIncomingCount <= current.ancestorImplementationCount
implMember.isDeleted === true
) { ) {
continue; continue;
} }
if (implMember.nodeId === primaryMemberDef.nodeId) continue; bestIncomingCount.set(subDef.nodeId, current.ancestorImplementationCount);
// A re-declared interface method or an `abstract` override is not an
// implementation — keep descending past it rather than emitting to it. const implMember = pickOverload(subDef.nodeId, memberName, site, model, provider);
if (isDeclarationOnly(implMember)) continue; let descendantImplementationCount = current.ancestorImplementationCount;
// Nor is a static member: no instance-typed receiver can reach one, so if (
// an edge to it is a target dispatch cannot produce (#2842 review). implMember !== undefined &&
if (isUnreachableByInstanceDispatch(implMember)) continue; implMember !== OVERLOAD_AMBIGUOUS &&
targets.push(implMember); implMember.isDeleted !== true &&
implMember.nodeId !== primaryMemberDef.nodeId &&
!isDeclarationOnly(implMember) &&
!isUnreachableByInstanceDispatch(implMember)
) {
const existing = targetByMemberId.get(implMember.nodeId);
const syntheticOwnerPriority = subDef.isSynthetic === true ? 1 : 0;
if (
existing === undefined ||
syntheticOwnerPriority < existing.syntheticOwnerPriority ||
(syntheticOwnerPriority === existing.syntheticOwnerPriority &&
current.ancestorImplementationCount < existing.ancestorImplementationCount)
) {
targetByMemberId.set(implMember.nodeId, {
member: implMember,
syntheticOwnerPriority,
ancestorImplementationCount: current.ancestorImplementationCount,
discoveryOrder: existing?.discoveryOrder ?? discoveryOrder++,
});
}
descendantImplementationCount++;
}
queue.push({
typeId: subDef.nodeId,
ancestorImplementationCount: descendantImplementationCount,
});
} }
} }
const targets = [...targetByMemberId.values()]
.sort(
(left, right) =>
left.syntheticOwnerPriority - right.syntheticOwnerPriority ||
left.ancestorImplementationCount - right.ancestorImplementationCount ||
left.discoveryOrder - right.discoveryOrder,
)
.map((target) => target.member);
// Bounded, and NEVER silently (#2829). An interface with a very large // Bounded, and NEVER silently (#2829). An interface with a very large
// implementor set multiplies edges by every call site — Go, TypeScript and // implementor set multiplies edges by every call site — Go, TypeScript and
// Kotlin do not set `collapseMemberCallsByCallerTarget`, so the product is // Kotlin do not set `collapseMemberCallsByCallerTarget`, so the product is
@ -505,8 +555,13 @@ export function emitReceiverBoundCalls(
if (targets.length > MAX_INTERFACE_DISPATCH_FANOUT) { if (targets.length > MAX_INTERFACE_DISPATCH_FANOUT) {
dispatchFanoutSkipped += targets.length - MAX_INTERFACE_DISPATCH_FANOUT; dispatchFanoutSkipped += targets.length - MAX_INTERFACE_DISPATCH_FANOUT;
if (dispatchFanoutSkippedNames.length < MAX_REPORTED_SKIPPED_INTERFACES) { if (dispatchFanoutSkippedNames.length < MAX_REPORTED_SKIPPED_INTERFACES) {
const dropped = targets
.slice(MAX_INTERFACE_DISPATCH_FANOUT, MAX_INTERFACE_DISPATCH_FANOUT + 5)
.map((target) => target.qualifiedName ?? target.nodeId);
const omitted = targets.length - MAX_INTERFACE_DISPATCH_FANOUT - dropped.length;
dispatchFanoutSkippedNames.push( dispatchFanoutSkippedNames.push(
`${ownerDef.qualifiedName ?? ownerDef.nodeId}.${memberName} (${targets.length} targets)`, `${ownerDef.qualifiedName ?? ownerDef.nodeId}.${memberName} (${targets.length} targets; ` +
`dropped: ${dropped.join(', ')}${omitted > 0 ? `, +${omitted} more` : ''})`,
); );
} }
targets.length = MAX_INTERFACE_DISPATCH_FANOUT; targets.length = MAX_INTERFACE_DISPATCH_FANOUT;

View file

@ -172,7 +172,10 @@ import {
SPRING_BEAN_INVENTORY_FEATURE, SPRING_BEAN_INVENTORY_FEATURE,
SPRING_CONDITIONALS_FEATURE, SPRING_CONDITIONALS_FEATURE,
} from './ingestion/frameworks/spring/analysis-features.js'; } from './ingestion/frameworks/spring/analysis-features.js';
import { SPRING_CONFIG_BINDINGS_FEATURE } from './ingestion/languages/java/analysis-features.js'; import {
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
SPRING_CONFIG_BINDINGS_FEATURE,
} from './ingestion/languages/java/analysis-features.js';
import { import {
CLASS_FRAMEWORK_ANNOTATIONS_FEATURE, CLASS_FRAMEWORK_ANNOTATIONS_FEATURE,
findAnalysisFeatureMismatches, findAnalysisFeatureMismatches,
@ -225,6 +228,7 @@ const ANALYSIS_FEATURES = [
SPRING_BEAN_INVENTORY_FEATURE, SPRING_BEAN_INVENTORY_FEATURE,
SPRING_CONDITIONALS_FEATURE, SPRING_CONDITIONALS_FEATURE,
SPRING_CONFIG_BINDINGS_FEATURE, SPRING_CONFIG_BINDINGS_FEATURE,
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
] as const; ] as const;
interface PersistedFrameworkAnnotationRow { interface PersistedFrameworkAnnotationRow {

View file

@ -467,7 +467,6 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j
// //
// Still open at this commit: #2891 also claims 59, which main now holds. That is // Still open at this commit: #2891 also claims 59, which main now holds. That is
// a live exact clash for #2891 to renumber, not for this branch. // a live exact clash for #2891 to renumber, not for this branch.
// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING.
// //
// 60 -> 62 for the two optional `ParsedImport` fields the cycle-checker fix // 60 -> 62 for the two optional `ParsedImport` fields the cycle-checker fix
// adds: `typeOnly` (TS `import type`) and `runsOnlyWhenCalled` (an import // adds: `typeOnly` (TS `import type`) and `runsOnlyWhenCalled` (an import
@ -503,10 +502,15 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j
// one, and the accessor definitions it materializes are not in this branch's // one, and the accessor definitions it materializes are not in this branch's
// ParsedFile shape at all. // ParsedFile shape at all.
// //
// Note for whoever merges next: #2935 and #2840 BOTH claim 61, independently of // 63 -> 64 for Java enum heritage plus annotated class, record, interface,
// this branch. That clash is still live and is theirs to resolve. // enum, and explicit-super base names emitting corrected captures (#2918).
// Warm v63 ParsedFiles lack those captures and must be re-extracted.
// 64 -> 66 adds the synthetic-declaration sidecar used to keep anonymous class
// implementations from evicting ordinary implementors at the dispatch cap.
// This PR already published a v64 head, while #2936 uses 65 for its independent
// record accessor shape, so 66 keeps all three cached shapes distinct.
// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING. // RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING.
const SCHEMA_BUMP = 63; const SCHEMA_BUMP = 66;
const GITNEXUS_PKG_VERSION = (() => { const GITNEXUS_PKG_VERSION = (() => {
try { try {
// package.json sits at gitnexus/package.json — two levels up from // package.json sits at gitnexus/package.json — two levels up from

View file

@ -5,6 +5,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import path from 'path'; import path from 'path';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os'; import os from 'node:os';
import { _captureLogger, type PinoLogRecord } from '../../../src/core/logger.js';
import { import {
FIXTURES, FIXTURES,
CROSS_FILE_FIXTURES, CROSS_FILE_FIXTURES,
@ -1341,6 +1342,150 @@ describe('Java record method resolution (#2564)', () => {
}, 60000); }, 60000);
}); });
describe('Java enum interface heritage (#2918)', () => {
it('links and dispatches an Enum interface method (#2918)', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-java-enum-heritage-'));
try {
writeFixtureRepo(root, {
'EnumHeritage.java': `import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE_USE) @interface Marker {}
interface Named { String label(); }
enum Status implements @Marker Named {
ACTIVE;
public String label() { return "active"; }
}
class Reader {
String read(Named value) { return value.label(); }
}`,
});
const linked = await runPipelineFromRepo(root, () => {});
const implementsEdges = getRelationships(linked, 'IMPLEMENTS').filter(
(edge) => edge.source === 'Status' && edge.target === 'Named',
);
const fanout = getRelationships(linked, 'CALLS').filter(
(edge) =>
edge.source === 'read' &&
edge.target === 'label' &&
edge.rel.reason === 'interface-dispatch',
);
expect(implementsEdges).toHaveLength(1);
expect(implementsEdges[0]?.sourceLabel).toBe('Enum');
expect(implementsEdges[0]?.targetLabel).toBe('Interface');
expect(fanout.map((edge) => edge.rel.targetId).sort()).toEqual([
'Method:EnumHeritage.java:Status.label#0',
]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}, 60000);
it('keeps enum constant-body methods distinct while preserving enum heritage (#2918)', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-java-enum-constant-body-'));
try {
writeFixtureRepo(root, {
'EnumConstantBody.java': `interface Named { String label(); }
enum Status implements Named {
ACTIVE { public String label() { return "active"; } },
INACTIVE;
public String label() { return "inactive"; }
}
class Reader {
String read(Named value) { return value.label(); }
}`,
});
const linked = await runPipelineFromRepo(root, () => {});
const implementsEdges = getRelationships(linked, 'IMPLEMENTS').filter(
(edge) => edge.source === 'Status' && edge.target === 'Named',
);
expect(implementsEdges).toHaveLength(1);
expect(implementsEdges[0]?.sourceLabel).toBe('Enum');
expect(getNodesByLabel(linked, 'Method').filter((name) => name === 'label')).toHaveLength(3);
const fanout = getRelationships(linked, 'CALLS').filter(
(edge) =>
edge.source === 'read' &&
edge.target === 'label' &&
edge.rel.reason === 'interface-dispatch',
);
expect(fanout.map((edge) => edge.rel.targetId).sort()).toEqual([
'Method:EnumConstantBody.java:Status$1.label#0',
'Method:EnumConstantBody.java:Status.label#0',
]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}, 60000);
it('keeps non-synthetic implementations ahead of abstract enum constant bodies at the cap', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-java-enum-fanout-cap-'));
try {
const constants = Array.from(
{ length: 40 },
(_, index) => `A${index} { public String label() { return "enum-${index}"; } }`,
).join(',\n');
const classes = Array.from(
{ length: 30 },
(_, index) =>
`class ZImpl${index} extends ZBase { public String label() { return "class-${index}"; } }`,
).join('\n');
writeFixtureRepo(root, {
'Fanout.java': `interface Named { String label(); }
enum AaaBig implements Named {
${constants};
public abstract String label();
}
abstract class ZBase implements Named { public abstract String label(); }
${classes}
class Reader { String read(Named value) { return value.label(); } }`,
});
const loggerCapture = _captureLogger();
let linked: PipelineResult;
let logRecords: PinoLogRecord[];
try {
linked = await runPipelineFromRepo(root, () => {});
logRecords = loggerCapture.records();
} finally {
loggerCapture.restore();
}
const fanoutIds = getRelationships(linked, 'CALLS')
.filter(
(edge) =>
edge.source === 'read' &&
edge.target === 'label' &&
edge.rel.reason === 'interface-dispatch',
)
.map((edge) => edge.rel.targetId);
expect(fanoutIds).toHaveLength(32);
for (let index = 0; index < 30; index++) {
expect(fanoutIds).toContain(`Method:Fanout.java:ZImpl${index}.label#0`);
}
expect(fanoutIds).toContain('Method:Fanout.java:AaaBig$1.label#0');
expect(fanoutIds).toContain('Method:Fanout.java:AaaBig$2.label#0');
const warning = logRecords.find(
(record) =>
record.msg ===
'interface-dispatch: members over the fan-out cap dropped implementors (their CALLS edges were not emitted)',
);
expect(warning).toMatchObject({
dispatchFanoutSkipped: 38,
fanoutCap: 32,
dispatchFanoutSkippedNames: [
'Named.label (70 targets; dropped: AaaBig$3.label, AaaBig$4.label, AaaBig$5.label, AaaBig$6.label, AaaBig$7.label, +33 more)',
],
});
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}, 60000);
});
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Java 16+ instanceof pattern variable: `if (obj instanceof User user)` // Java 16+ instanceof pattern variable: `if (obj instanceof User user)`
// Phase 5.2: extractPatternBinding on instanceof_expression binds user → User. // Phase 5.2: extractPatternBinding on instanceof_expression binds user → User.

View file

@ -10,7 +10,10 @@ import {
SPRING_BEAN_INVENTORY_FEATURE, SPRING_BEAN_INVENTORY_FEATURE,
SPRING_CONDITIONALS_FEATURE, SPRING_CONDITIONALS_FEATURE,
} from '../../src/core/ingestion/frameworks/spring/analysis-features.js'; } from '../../src/core/ingestion/frameworks/spring/analysis-features.js';
import { SPRING_CONFIG_BINDINGS_FEATURE } from '../../src/core/ingestion/languages/java/analysis-features.js'; import {
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
SPRING_CONFIG_BINDINGS_FEATURE,
} from '../../src/core/ingestion/languages/java/analysis-features.js';
const FEATURES = [ const FEATURES = [
CLASS_FRAMEWORK_ANNOTATIONS_FEATURE, CLASS_FRAMEWORK_ANNOTATIONS_FEATURE,
@ -18,6 +21,7 @@ const FEATURES = [
SPRING_BEAN_INVENTORY_FEATURE, SPRING_BEAN_INVENTORY_FEATURE,
SPRING_CONDITIONALS_FEATURE, SPRING_CONDITIONALS_FEATURE,
SPRING_CONFIG_BINDINGS_FEATURE, SPRING_CONFIG_BINDINGS_FEATURE,
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
] as const; ] as const;
describe('analysis feature versions', () => { describe('analysis feature versions', () => {
@ -27,6 +31,7 @@ describe('analysis feature versions', () => {
}); });
expect(resolveAnalysisFeatureVersions(FEATURES, ['src/App.java'])).toEqual({ expect(resolveAnalysisFeatureVersions(FEATURES, ['src/App.java'])).toEqual({
'graph.class-framework-annotations': 1, 'graph.class-framework-annotations': 1,
'java.heritage-captures': 1,
'spring.aop-advice': 1, 'spring.aop-advice': 1,
'spring.bean-inventory': 2, 'spring.bean-inventory': 2,
'spring.conditionals-auto-configuration': 1, 'spring.conditionals-auto-configuration': 1,

View file

@ -53,7 +53,10 @@ import {
SPRING_AOP_EVIDENCE_ID_PREFIX, SPRING_AOP_EVIDENCE_ID_PREFIX,
} from '../../src/core/ingestion/frameworks/spring/aop.js'; } from '../../src/core/ingestion/frameworks/spring/aop.js';
import { SPRING_AUTO_CONFIGURATION_SYNTHETIC_ID_PREFIX } from '../../src/core/ingestion/frameworks/spring/auto-configuration.js'; import { SPRING_AUTO_CONFIGURATION_SYNTHETIC_ID_PREFIX } from '../../src/core/ingestion/frameworks/spring/auto-configuration.js';
import { SPRING_CONFIG_BINDINGS_FEATURE } from '../../src/core/ingestion/languages/java/analysis-features.js'; import {
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
SPRING_CONFIG_BINDINGS_FEATURE,
} from '../../src/core/ingestion/languages/java/analysis-features.js';
const setupMiniRepo = () => setupSharedMiniRepo('gitnexus-incr-orch-'); const setupMiniRepo = () => setupSharedMiniRepo('gitnexus-incr-orch-');
@ -96,6 +99,24 @@ async function setupSpringBeanIncrementalRepo() {
return repo; return repo;
} }
async function setupJavaEnumHeritageIncrementalRepo() {
const repo = await createTempDir('gitnexus-incr-java-enum-heritage-');
const src = path.join(repo.dbPath, 'src');
await mkdir(src, { recursive: true });
await writeFile(
path.join(src, 'Status.java'),
'interface Named { String label(); }\n' +
'enum Status implements Named {\n' +
' ACTIVE;\n' +
' public String label() { return "active"; }\n' +
'}\n',
'utf-8',
);
execSync('git init', { cwd: repo.dbPath, stdio: 'pipe' });
gitCommitAll(repo.dbPath, 'initial Java enum heritage');
return repo;
}
async function setupKotlinSpringBeanIncrementalRepo() { async function setupKotlinSpringBeanIncrementalRepo() {
const repo = await createTempDir('gitnexus-incr-spring-bean-kotlin-'); const repo = await createTempDir('gitnexus-incr-spring-bean-kotlin-');
const src = path.join(repo.dbPath, 'src', 'com', 'other'); const src = path.join(repo.dbPath, 'src', 'com', 'other');
@ -224,6 +245,35 @@ async function readSpringConfigPropertyNames(repoPath: string): Promise<string[]
} }
} }
async function countStatusImplementsNamed(repoPath: string): Promise<number> {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
const { lbugPath } = getStoragePaths(repoPath);
await adapter.initLbug(lbugPath);
try {
const rows = (await adapter.executeQuery(
"MATCH (e:Enum {name: 'Status'})-[r:CodeRelation]->(i:Interface {name: 'Named'}) " +
"WHERE r.type = 'IMPLEMENTS' RETURN count(r) AS c",
)) as Array<{ c: number | bigint }>;
return Number(rows[0]?.c ?? 0);
} finally {
await adapter.closeLbug();
}
}
async function deleteStatusImplementsNamed(repoPath: string): Promise<void> {
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
const { lbugPath } = getStoragePaths(repoPath);
await adapter.initLbug(lbugPath);
try {
await adapter.executeQuery(
"MATCH (e:Enum {name: 'Status'})-[r:CodeRelation]->(i:Interface {name: 'Named'}) " +
"WHERE r.type = 'IMPLEMENTS' DELETE r",
);
} finally {
await adapter.closeLbug();
}
}
/** /**
* Direct count over INJECTS CodeRelation rows mirrors pdg-mode-flip's * Direct count over INJECTS CodeRelation rows mirrors pdg-mode-flip's
* countBasicBlocks: reopen the repo DB, count, close (runFullAnalysis closes * countBasicBlocks: reopen the repo DB, count, close (runFullAnalysis closes
@ -465,6 +515,43 @@ describe('runFullAnalysis — incremental orchestration', () => {
} }
}, 300_000); }, 300_000);
it('a Java index missing enum heritage evidence rebuilds before the fast path (#2918)', async () => {
const repo = await setupJavaEnumHeritageIncrementalRepo();
try {
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
await runFullAnalysis(repo.dbPath, { skipAgentsMd: true }, { onProgress: () => {} });
const { storagePath } = getStoragePaths(repo.dbPath);
const meta = await loadMeta(storagePath);
expect(meta!.analysisFeatures).toMatchObject({
[JAVA_ENUM_INTERFACE_HERITAGE_FEATURE.id]: JAVA_ENUM_INTERFACE_HERITAGE_FEATURE.version,
});
expect(await countStatusImplementsNamed(repo.dbPath)).toBe(1);
await deleteStatusImplementsNamed(repo.dbPath);
expect(await countStatusImplementsNamed(repo.dbPath)).toBe(0);
await saveMeta(
storagePath,
withoutAnalysisFeature(meta!, JAVA_ENUM_INTERFACE_HERITAGE_FEATURE.id),
);
const logs: string[] = [];
const reanalyzed = await runFullAnalysis(
repo.dbPath,
{ skipAgentsMd: true },
{ onProgress: () => {}, onLog: (message) => logs.push(message) },
);
expect(reanalyzed.alreadyUpToDate).toBeUndefined();
expect(logs.join('\n')).toContain(`missing:${JAVA_ENUM_INTERFACE_HERITAGE_FEATURE.id}`);
expect((await loadMeta(storagePath))!.analysisFeatures).toMatchObject({
[JAVA_ENUM_INTERFACE_HERITAGE_FEATURE.id]: JAVA_ENUM_INTERFACE_HERITAGE_FEATURE.version,
});
expect(await countStatusImplementsNamed(repo.dbPath)).toBe(1);
} finally {
await repo.cleanup();
}
}, 300_000);
it('a same-commit index with NO fingerprint (pre-#2798) rebuilds once, not grandfathered', async () => { it('a same-commit index with NO fingerprint (pre-#2798) rebuilds once, not grandfathered', async () => {
const repo = await setupMiniRepo(); const repo = await setupMiniRepo();
try { try {

View file

@ -204,24 +204,25 @@ describe('PARSE_CACHE_VERSION', () => {
// would take the untagged path, and `check --cycles` would keep reporting the // would take the untagged path, and `check --cycles` would keep reporting the
// erased and deferred imports the branch exists to stop reporting: a silent // erased and deferred imports the branch exists to stop reporting: a silent
// no-op on incremental analyze while every cold-run test passes. // no-op on incremental analyze while every cold-run test passes.
// 63 rather than 62 or 61: main holds 60, #2935 claims 61, and #2936 claims 62 // Main subsequently advanced through 63. Values above it must remain distinct
// — the next free value above every in-flight MAXIMUM, not above origin/main. // from both published branch heads and every active in-flight claim.
// This branch staged 62 first and was correct when written; #2936 opened four // Moved 63 -> 64 for Java enum and annotated heritage captures (#2918),
// hours later, re-checked against main rather than the in-flight claims, and // then 64 -> 66 for the synthetic-declaration sidecar. #2936 uses 65 for
// took 62 as well. Moving instead of standing on seniority, because 63 is // its independent record-component accessor cache shape.
// right whichever of the two merges first. it('pins SCHEMA_BUMP to 66 so concurrent bumps cannot silently collide (#2766)', () => {
it('pins SCHEMA_BUMP to 63 so concurrent bumps cannot silently collide (#2766)', () => { expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(66);
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(63);
// The PREVIOUS version must fail the reuse gate, not merely differ from the // The PREVIOUS version must fail the reuse gate, not merely differ from the
// current one — a hardcoded number outside the conflict hunk rebases cleanly // current one — a hardcoded number outside the conflict hunk rebases cleanly
// while being wrong, which is exactly how the 37/38 exact clashes landed. // while being wrong, which is exactly how the 37/38 exact clashes landed.
// Every live neighbour is named: 60 is what origin/main holds, so a rebase // Every nearby historical value is rejected: origin/main advanced through
// that drops this branch's bump lands there; 61 is claimed by BOTH #2935 and // 63, while this branch already published 64 and #2936 uses 65. Pinning 66
// #2840 (a live clash of their own); and 62 is #2936's claim, which is what // and rejecting all prior values makes an accidental conflict resolution loud.
// this value moved off.
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(60); expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(60);
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(61); expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(61);
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(62); expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(62);
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(63);
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(64);
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(65);
}); });
it('embeds the gitnexus package version (so upgrades invalidate the cache)', () => { it('embeds the gitnexus package version (so upgrades invalidate the cache)', () => {

View file

@ -406,6 +406,7 @@ describe('parsedfile-store', () => {
filePath: 'a.c', filePath: 'a.c',
type: 'Function', type: 'Function',
qualifiedName: 'fn', qualifiedName: 'fn',
isSynthetic: true,
}; };
const pf = { const pf = {
filePath: 'a.c', filePath: 'a.c',
@ -445,6 +446,7 @@ describe('parsedfile-store', () => {
filePath: 'a.c', filePath: 'a.c',
type: 'Function', type: 'Function',
qualifiedName: 'fn', qualifiedName: 'fn',
isSynthetic: true,
}); });
} finally { } finally {
await rm(dir, { recursive: true, force: true }); await rm(dir, { recursive: true, force: true });

View file

@ -90,7 +90,7 @@ describe('emitJavaScopeCaptures — constructor reference names (F35 #1928)', ()
}); });
}); });
describe('emitJavaScopeCaptures — record interface heritage (#2900)', () => { describe('emitJavaScopeCaptures — record and enum interface heritage (#2900, #2918)', () => {
it('captures simple, generic, and qualified record interfaces by lookup name', () => { it('captures simple, generic, and qualified record interfaces by lookup name', () => {
const refs = inheritanceRefs( const refs = inheritanceRefs(
'record User(int id) implements Named, Comparable<User>, audit.Auditable {}', 'record User(int id) implements Named, Comparable<User>, audit.Auditable {}',
@ -99,9 +99,41 @@ describe('emitJavaScopeCaptures — record interface heritage (#2900)', () => {
expect(refs).toEqual(['Auditable', 'Comparable', 'Named']); expect(refs).toEqual(['Auditable', 'Comparable', 'Named']);
}); });
it('does not yet emit enum heritage (#2918)', () => { it('captures simple, generic, and qualified enum interfaces by lookup name', () => {
// Delete this characterization when #2918 adds enum interface heritage. const refs = inheritanceRefs(
expect(inheritanceRefs('enum Status implements Named { ACTIVE }')).toEqual([]); 'enum Status implements Named, Tagged<Status>, audit.Auditable { ACTIVE }',
);
expect(refs).toEqual(['Auditable', 'Named', 'Tagged']);
});
it('unwraps type-use annotations on enum interface names', () => {
const refs = inheritanceRefs(
'enum Status implements @Marker Named, @Marker Tagged<Status>, audit.@Marker Auditable { ACTIVE }',
);
expect(refs).toEqual(['Auditable', 'Named', 'Tagged']);
});
it.each([
['class extends', 'class Child extends @Marker Base {}', ['Base']],
['class implements', 'class Child implements @Marker Named {}', ['Named']],
['record implements', 'record Child(int id) implements @Marker Named {}', ['Named']],
['interface extends', 'interface Child extends @Marker Named {}', ['Named']],
])('unwraps type-use annotations for %s', (_label, source, expected) => {
expect(inheritanceRefs(source)).toEqual(expected);
});
it('does not emit an empty inheritance name from a torn annotated base', () => {
expect(inheritanceRefs('enum Status implements @Marker {')).toEqual([]);
});
it('preserves the enum constant-body link while adding enum interface heritage', () => {
expect(
inheritanceRefs(
'enum Status implements Named { ACTIVE { public String label() { return "active"; } } }',
),
).toEqual(['Named', 'Status']);
}); });
}); });
@ -120,6 +152,11 @@ describe('emitJavaScopeCaptures — explicit constructor invocations (F38 #1928)
expect(refs.some((r) => r.name === 'Box' && r.arity === '0')).toBe(true); expect(refs.some((r) => r.name === 'Box' && r.arity === '0')).toBe(true);
}); });
it('unwraps an annotated superclass for explicit `super(...)`', () => {
const refs = ctorRefs('class C extends @Marker Base { C() { super(); } }');
expect(refs.some((r) => r.name === 'Base' && r.arity === '0')).toBe(true);
});
it('captures `this(...)` as a constructor ref to the enclosing class name', () => { it('captures `this(...)` as a constructor ref to the enclosing class name', () => {
const src = 'class C { C() { this(1); } C(int x) {} }'; const src = 'class C { C() { this(1); } C(int x) {} }';
const refs = ctorRefs(src); const refs = ctorRefs(src);

View file

@ -261,6 +261,22 @@ describe('Pass 2: declarations + local bindings', () => {
expect(result.localDefs[0]!.type).toBe('Function'); expect(result.localDefs[0]!.type).toBe('Function');
}); });
it('preserves a synthetic declaration marker on the definition', () => {
const result = extract(
[
scopeMatch('module', 1, 0, 100, 0),
declMatch('class', 'Worker$1', 5, 0, 10, 0, {
'@declaration.is-synthetic': cap('@declaration.is-synthetic', 5, 0, 10, 0, 'true'),
}),
],
'a.ts',
mockProvider(),
);
expect(result.localDefs).toHaveLength(1);
expect(result.localDefs[0]!.isSynthetic).toBe(true);
});
it('honors `provider.bindingScopeFor` to hoist a binding to an outer scope', () => { it('honors `provider.bindingScopeFor` to hoist a binding to an outer scope', () => {
// Treat every declaration as hoisted to the module scope. // Treat every declaration as hoisted to the module scope.
const result = extract( const result = extract(