mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-22 00:31:17 +00:00
feat(csharp-scope): parity Unit 3b — constructor CALLS emission
Closes 3 parity failures (25 → 22). Adds constructor-form CALLS edge emission + C# 12 primary constructor synthesis. Changes: - `scope-resolution/passes/free-call-fallback.ts`: when a site's callForm === 'constructor', look up the class def (not a callable) and pick its explicit Constructor def via workspaceIndex's memberByOwner — or fall back to the Class def itself for implicit constructors. Matches legacy behavior (targetLabel === 'Constructor' when explicit, 'Class' when implicit). - `scope-resolution/pipeline/run.ts`: pass workspaceIndex to the free-call fallback. - `languages/csharp/captures.ts`: synthesize @declaration.constructor for C# 12 primary constructors — `class User(string name, int age)` / `record Person(string First, string Last)`. The parameter_list is a named child of the class_declaration / record_declaration (not a separate constructor_declaration node). Skip the synthesis when the type already has an explicit constructor to avoid duplicates. Emits @declaration.parameter-count + required-parameter-count alongside. Legacy 175/175 green; 376/376 scope-resolution unit tests pass; 22 parity failures remain.
This commit is contained in:
parent
cb8a268031
commit
f0805640e9
3 changed files with 108 additions and 3 deletions
|
|
@ -144,11 +144,82 @@ export function emitCsharpScopeCaptures(
|
|||
}
|
||||
|
||||
out.push(grouped);
|
||||
|
||||
// Synthesize primary-constructor declarations on class/record
|
||||
// declarations that carry a `parameter_list` child (C# 12 syntax
|
||||
// `public class User(string name, int age) { ... }` or
|
||||
// `public record Person(string FirstName, string LastName)`).
|
||||
// Legacy `csharpMethodConfig.extractPrimaryConstructor` runs via
|
||||
// the parse phase; the scope-resolution path needs its own emit so
|
||||
// `new User(...)` resolves to a Constructor def in memberByOwner.
|
||||
if (
|
||||
grouped['@declaration.class'] !== undefined ||
|
||||
grouped['@declaration.record'] !== undefined
|
||||
) {
|
||||
const anchor = grouped['@declaration.class'] ?? grouped['@declaration.record']!;
|
||||
const typeNode =
|
||||
findNodeAtRange(tree.rootNode, anchor.range, 'class_declaration') ??
|
||||
findNodeAtRange(tree.rootNode, anchor.range, 'record_declaration');
|
||||
if (typeNode !== null) {
|
||||
const synth = synthesizePrimaryConstructor(typeNode);
|
||||
if (synth !== null) out.push(synth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** C# 12 primary constructor: `class X(a, b) { }` / `record X(a, b)`.
|
||||
* The parameters are a bare `parameter_list` named child of the type
|
||||
* declaration (no `constructor_declaration` node). Emit a synthetic
|
||||
* @declaration.constructor match so the extractor creates a
|
||||
* Constructor def in memberByOwner — free-call-fallback's
|
||||
* `pickConstructorOrClass` then targets it for `new X(...)` calls. */
|
||||
function synthesizePrimaryConstructor(typeNode: SyntaxNode): CaptureMatch | null {
|
||||
// Skip types with an explicit constructor_declaration — that would
|
||||
// create duplicate defs.
|
||||
const body = typeNode.childForFieldName('body');
|
||||
if (body !== null) {
|
||||
for (let i = 0; i < body.namedChildCount; i++) {
|
||||
const child = body.namedChild(i);
|
||||
if (child !== null && child.type === 'constructor_declaration') return null;
|
||||
}
|
||||
}
|
||||
let paramList: SyntaxNode | null = null;
|
||||
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
||||
const child = typeNode.namedChild(i);
|
||||
if (child !== null && child.type === 'parameter_list') {
|
||||
paramList = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (paramList === null) return null;
|
||||
|
||||
const nameNode = typeNode.childForFieldName('name');
|
||||
if (nameNode === null) return null;
|
||||
|
||||
const paramCount = paramList.namedChildren.filter(
|
||||
(c) => c !== null && c.type === 'parameter',
|
||||
).length;
|
||||
|
||||
const m: Record<string, Capture> = {
|
||||
'@declaration.constructor': nodeToCapture('@declaration.constructor', paramList),
|
||||
'@declaration.name': syntheticCapture('@declaration.name', nameNode, nameNode.text),
|
||||
'@declaration.parameter-count': syntheticCapture(
|
||||
'@declaration.parameter-count',
|
||||
paramList,
|
||||
String(paramCount),
|
||||
),
|
||||
'@declaration.required-parameter-count': syntheticCapture(
|
||||
'@declaration.required-parameter-count',
|
||||
paramList,
|
||||
String(paramCount),
|
||||
),
|
||||
};
|
||||
return m;
|
||||
}
|
||||
|
||||
type SyntaxNode = ReturnType<ReturnType<typeof getCsharpParser>['parse']>['rootNode'];
|
||||
|
||||
/** Find the first C# function-like node at the given range. The
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@
|
|||
* generalization plan.
|
||||
*/
|
||||
|
||||
import type { ParsedFile, Reference, ScopeId } from 'gitnexus-shared';
|
||||
import type { ParsedFile, Reference, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { KnowledgeGraph } from '../../../graph/types.js';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
||||
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
||||
import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js';
|
||||
import { findCallableBindingInScope } from '../scope/walkers.js';
|
||||
import { findCallableBindingInScope, findClassBindingInScope } from '../scope/walkers.js';
|
||||
|
||||
export function emitFreeCallFallback(
|
||||
graph: KnowledgeGraph,
|
||||
|
|
@ -31,6 +32,7 @@ export function emitFreeCallFallback(
|
|||
nodeLookup: GraphNodeLookup,
|
||||
_referenceIndex: { readonly bySourceScope: ReadonlyMap<ScopeId, readonly Reference[]> },
|
||||
handledSites: Set<string>,
|
||||
workspaceIndex?: WorkspaceResolutionIndex,
|
||||
): number {
|
||||
let emitted = 0;
|
||||
const seen = new Set<string>();
|
||||
|
|
@ -40,7 +42,20 @@ export function emitFreeCallFallback(
|
|||
if (site.kind !== 'call') continue;
|
||||
if (site.explicitReceiver !== undefined) continue;
|
||||
|
||||
const fnDef = findCallableBindingInScope(site.inScope, site.name, scopes);
|
||||
// Constructor form (`new User(...)`): resolve the class, then
|
||||
// emit CALLS to its explicit Constructor def (when present) or
|
||||
// to the Class node itself (implicit constructor). Legacy emits
|
||||
// the same two targets; see test expectations.
|
||||
let fnDef: SymbolDefinition | undefined;
|
||||
if (site.callForm === 'constructor') {
|
||||
const classDef = findClassBindingInScope(site.inScope, site.name, scopes);
|
||||
if (classDef !== undefined) {
|
||||
fnDef = pickConstructorOrClass(classDef, workspaceIndex);
|
||||
}
|
||||
}
|
||||
if (fnDef === undefined) {
|
||||
fnDef = findCallableBindingInScope(site.inScope, site.name, scopes);
|
||||
}
|
||||
if (fnDef === undefined) continue;
|
||||
|
||||
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
|
||||
|
|
@ -69,3 +84,21 @@ export function emitFreeCallFallback(
|
|||
}
|
||||
return emitted;
|
||||
}
|
||||
|
||||
/** For a constructor call `new X(...)`, return the X class's explicit
|
||||
* Constructor def (by walking memberByOwner) or the Class def itself
|
||||
* when no explicit Constructor exists. Matches legacy behavior —
|
||||
* tests assert targetLabel === 'Class' for implicit ctors and
|
||||
* targetLabel === 'Constructor' for explicit ones. */
|
||||
function pickConstructorOrClass(
|
||||
classDef: SymbolDefinition,
|
||||
workspaceIndex: WorkspaceResolutionIndex | undefined,
|
||||
): SymbolDefinition {
|
||||
if (workspaceIndex === undefined) return classDef;
|
||||
const members = workspaceIndex.memberByOwner.get(classDef.nodeId);
|
||||
if (members === undefined) return classDef;
|
||||
for (const [, def] of members) {
|
||||
if (def.type === 'Constructor') return def;
|
||||
}
|
||||
return classDef;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ export function runScopeResolution(
|
|||
nodeLookup,
|
||||
referenceIndex,
|
||||
handledSites,
|
||||
workspaceIndex,
|
||||
);
|
||||
const { emitted, skipped } = emitReferencesViaLookup(
|
||||
graph,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue