mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(scope-resolution): fan out C# Record interface calls (#2904)
Some checks failed
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Has been cancelled
Some checks failed
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Has been cancelled
* fix(scope-resolution): fan out C# Record interface calls Use the shared class-like predicate so canonical C# Record implementors participate in interface dispatch, and pin the missing call edge with a regression test. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scope-resolution): preserve partial Record dispatch Keep every scope definition that shares a graph node so interface fan-out is independent of partial declaration order, and strengthen C# dispatch controls. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
parent
81100e2c74
commit
49c5b7d81f
2 changed files with 95 additions and 12 deletions
|
|
@ -326,17 +326,22 @@ export function emitReceiverBoundCalls(
|
|||
isBuiltInName: options.isBuiltInName,
|
||||
};
|
||||
|
||||
// Build an interface → implementors map from IMPLEMENTS edges.
|
||||
// Maps Interface graph-id → list of implementor class scope-def-ids.
|
||||
// We translate graph-ids back to scope-resolution DefIds via
|
||||
// `parsedFiles.localDefs` lookup so downstream `findOwnedMember`
|
||||
// (which keys by DefId) can find the implementor's members.
|
||||
const graphIdToClassDef = new Map<string, SymbolDefinition>();
|
||||
// Maps class-like graph ids back to ALL scope definitions that resolved to
|
||||
// them. Same-file partial declarations share one graph id but keep distinct
|
||||
// DefIds, and `pickOverload` keys member lookup by those DefIds. Preserving
|
||||
// every part makes dispatch independent of declaration order.
|
||||
const graphIdToClassDefs = new Map<string, SymbolDefinition[]>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const def of parsed.localDefs) {
|
||||
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue;
|
||||
if (!isClassLike(def.type)) continue;
|
||||
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
|
||||
if (graphId !== undefined) graphIdToClassDef.set(graphId, def);
|
||||
if (graphId === undefined) continue;
|
||||
let defs = graphIdToClassDefs.get(graphId);
|
||||
if (defs === undefined) {
|
||||
defs = [];
|
||||
graphIdToClassDefs.set(graphId, defs);
|
||||
}
|
||||
defs.push(def);
|
||||
}
|
||||
}
|
||||
// Direct subtypes of a type, keyed by the SUPERtype's def id.
|
||||
|
|
@ -360,10 +365,12 @@ export function emitReceiverBoundCalls(
|
|||
};
|
||||
for (const relType of ['IMPLEMENTS', 'EXTENDS'] as const) {
|
||||
for (const rel of graph.iterRelationshipsByType(relType)) {
|
||||
const superDef = graphIdToClassDef.get(rel.targetId);
|
||||
const subDef = graphIdToClassDef.get(rel.sourceId);
|
||||
if (superDef === undefined || subDef === undefined) continue;
|
||||
addSubtype(superDef.nodeId, subDef);
|
||||
const superDefs = graphIdToClassDefs.get(rel.targetId);
|
||||
const subDefs = graphIdToClassDefs.get(rel.sourceId);
|
||||
if (superDefs === undefined || subDefs === undefined) continue;
|
||||
for (const superDef of superDefs) {
|
||||
for (const subDef of subDefs) addSubtype(superDef.nodeId, subDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2353,6 +2353,82 @@ describe('C# record base resolution (record inheritance + base.Save)', () => {
|
|||
}
|
||||
}, 60000);
|
||||
|
||||
it('fans interface calls out to an implementing Record method (#2884)', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-csharp-record-dispatch-'));
|
||||
try {
|
||||
writeFixtureRepo(root, {
|
||||
'INamed.cs': 'namespace Probe; public interface INamed { string Name(); }',
|
||||
'User.cs': `namespace Probe;
|
||||
public record User(string Value) : INamed {
|
||||
public string Name() => Value;
|
||||
}`,
|
||||
'Reader.cs': `namespace Probe;
|
||||
public class Reader {
|
||||
public string Read(INamed value) => value.Name();
|
||||
public string ReadConcrete(User value) => value.Name();
|
||||
}`,
|
||||
});
|
||||
|
||||
const linked = await runPipelineFromRepo(root, () => {});
|
||||
const calls = getRelationships(linked, 'CALLS');
|
||||
const primary = calls.filter(
|
||||
(edge) =>
|
||||
edge.source === 'Read' &&
|
||||
edge.target === 'Name' &&
|
||||
edge.rel.reason !== 'interface-dispatch',
|
||||
);
|
||||
const fanout = calls.filter(
|
||||
(edge) =>
|
||||
edge.source === 'Read' &&
|
||||
edge.target === 'Name' &&
|
||||
edge.rel.reason === 'interface-dispatch',
|
||||
);
|
||||
const concreteFanout = calls.filter(
|
||||
(edge) =>
|
||||
edge.source === 'ReadConcrete' &&
|
||||
edge.target === 'Name' &&
|
||||
edge.rel.reason === 'interface-dispatch',
|
||||
);
|
||||
|
||||
expect(primary.map((edge) => `${edge.targetLabel}:${edge.targetFilePath}`)).toEqual([
|
||||
'Method:INamed.cs',
|
||||
]);
|
||||
expect(fanout.map((edge) => `${edge.targetLabel}:${edge.targetFilePath}`)).toEqual([
|
||||
'Method:User.cs',
|
||||
]);
|
||||
expect(concreteFanout).toEqual([]);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
it('fans out through reversed same-file partial Record declarations (#2884)', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-csharp-partial-record-dispatch-'));
|
||||
try {
|
||||
writeFixtureRepo(root, {
|
||||
'All.cs': `namespace Probe;
|
||||
public interface INamed { string Name(); }
|
||||
public partial record User { public string Name() => "u"; }
|
||||
public partial record User : INamed { }
|
||||
public class Reader { public string Read(INamed value) => value.Name(); }`,
|
||||
});
|
||||
|
||||
const linked = await runPipelineFromRepo(root, () => {});
|
||||
const fanout = getRelationships(linked, 'CALLS').filter(
|
||||
(edge) =>
|
||||
edge.source === 'Read' &&
|
||||
edge.target === 'Name' &&
|
||||
edge.rel.reason === 'interface-dispatch',
|
||||
);
|
||||
|
||||
expect(fanout.map((edge) => `${edge.targetLabel}:${edge.targetFilePath}`)).toEqual([
|
||||
'Method:All.cs',
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
it('resolves base.Save() inside UserRecord.Save to BaseEntity.Save (not self)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const baseSave = calls.find(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue