mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
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:
parent
25e51eac96
commit
cdc98a9cf8
15 changed files with 448 additions and 58 deletions
|
|
@ -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`)
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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')),
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)', () => {
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue