mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
feat(csharp-scope): Unit 5 — arity metadata synthesis + compatibility
Adversarial review flagged overload narrowing as a blocker for the Unit
7 flip. This lands the declaration-side metadata; callsite-side arity
synthesis is a separate gap we'll address if the parity gate surfaces
overload misresolution.
- `arity-metadata.ts` — reads `csharpMethodConfig.extractParameters`
and produces `{ parameterCount, requiredParameterCount,
parameterTypes }`. `params` variadic collapses parameterCount to
undefined (matches Python's `*args` treatment) and appends a literal
`'params'` marker to parameterTypes so the compatibility hook can
detect it without re-reading the AST. Default-valued parameters
contribute to optionalCount → requiredParameterCount = total − optional.
- `arity.ts` — `csharpArityCompatibility(def, callsite)` returns
compatible / incompatible / unknown. Mirrors Python's three-verdict
shape so the central registry's arity filter works without adapter
logic per-verdict.
- `captures.ts` — on every @declaration.method / @declaration.constructor
/ @declaration.function match, synthesize
@declaration.parameter-count, @declaration.required-parameter-count,
and @declaration.parameter-types captures. Covers method_declaration,
constructor_declaration, destructor_declaration, operator_declaration,
conversion_operator_declaration, and local_function_statement.
12 new tests: 5 on captures-side synthesis (method + params + types +
variadic + constructor + local function), 7 on the compatibility hook.
66/66 C# scope-resolution unit tests pass; tsc clean.
This commit is contained in:
parent
3c79058088
commit
ac8b55ba2c
5 changed files with 275 additions and 1 deletions
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Extract C# arity metadata from a method-like tree-sitter node —
|
||||
* `method_declaration`, `constructor_declaration`, `destructor_declaration`,
|
||||
* `operator_declaration`, `conversion_operator_declaration`, or
|
||||
* `local_function_statement`.
|
||||
*
|
||||
* Reuses `csharpMethodConfig.extractParameters` so scope-extracted defs
|
||||
* carry the same arity semantics as the legacy parse-worker path:
|
||||
* - `params` variadic collapses `parameterCount` to `undefined`,
|
||||
* which `csharpArityCompatibility` then treats as "max unknown" —
|
||||
* the candidate stays eligible at `argCount >= required`.
|
||||
* - Defaulted parameters (`= expr`) contribute to `optionalCount`;
|
||||
* `requiredParameterCount = total − optionalCount`.
|
||||
* - `parameterTypes` collects declared type names (with `ref`/`out`/
|
||||
* `in` prefix) for overload narrowing; a literal `'params'` marker
|
||||
* is appended for variadic methods so `csharpArityCompatibility`
|
||||
* can detect them without re-reading the AST.
|
||||
*/
|
||||
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { csharpMethodConfig } from '../../method-extractors/configs/csharp.js';
|
||||
|
||||
interface CsharpArityMetadata {
|
||||
readonly parameterCount: number | undefined;
|
||||
readonly requiredParameterCount: number | undefined;
|
||||
readonly parameterTypes: readonly string[] | undefined;
|
||||
}
|
||||
|
||||
export function computeCsharpArityMetadata(fnNode: SyntaxNode): CsharpArityMetadata {
|
||||
const params = csharpMethodConfig.extractParameters?.(fnNode) ?? [];
|
||||
|
||||
let hasVariadic = false;
|
||||
let optionalCount = 0;
|
||||
const types: string[] = [];
|
||||
for (const p of params) {
|
||||
if (p.isVariadic) hasVariadic = true;
|
||||
else if (p.isOptional) optionalCount++;
|
||||
if (p.type !== null) types.push(p.type);
|
||||
}
|
||||
if (hasVariadic) types.push('params');
|
||||
|
||||
const total = params.length;
|
||||
// `params int[] args` declares one formal param but accepts any arg
|
||||
// count ≥ required — mirror Python's treatment of `*args` and leave
|
||||
// `parameterCount` undefined so the registry treats max as unknown.
|
||||
const parameterCount = hasVariadic ? undefined : total;
|
||||
const requiredParameterCount = hasVariadic ? undefined : total - optionalCount;
|
||||
|
||||
return {
|
||||
parameterCount,
|
||||
requiredParameterCount,
|
||||
parameterTypes: types.length > 0 ? types : undefined,
|
||||
};
|
||||
}
|
||||
44
gitnexus/src/core/ingestion/languages/csharp/arity.ts
Normal file
44
gitnexus/src/core/ingestion/languages/csharp/arity.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* C# arity check, accommodating `params` variadic and default parameters.
|
||||
*
|
||||
* The `def` metadata we care about (synthesized by `arity-metadata.ts`):
|
||||
* - `parameterCount` — total formal parameters; `undefined`
|
||||
* when the method has `params T[]` variadic.
|
||||
* - `requiredParameterCount` — min required (excludes defaulted params
|
||||
* and `params` variadic).
|
||||
* - `parameterTypes` — declared type strings; contains the
|
||||
* literal `'params'` when the method is
|
||||
* variadic.
|
||||
*
|
||||
* Verdicts:
|
||||
* - `'compatible'` — `requiredParameterCount <= argCount <= parameterCount`,
|
||||
* OR the def takes `params` (then any `argCount >= required`).
|
||||
* - `'incompatible'` — argCount is below required, OR above max with no variadic.
|
||||
* - `'unknown'` — metadata is absent / incomplete.
|
||||
*
|
||||
* `'incompatible'` is a soft signal in `Registry.lookup` (penalized but
|
||||
* still considered when no compatible candidate exists), per RFC §4.
|
||||
*/
|
||||
|
||||
import type { Callsite, SymbolDefinition } from 'gitnexus-shared';
|
||||
|
||||
export function csharpArityCompatibility(
|
||||
def: SymbolDefinition,
|
||||
callsite: Callsite,
|
||||
): 'compatible' | 'unknown' | 'incompatible' {
|
||||
const max = def.parameterCount;
|
||||
const min = def.requiredParameterCount;
|
||||
if (max === undefined && min === undefined) return 'unknown';
|
||||
|
||||
const argCount = callsite.arity;
|
||||
if (!Number.isFinite(argCount) || argCount < 0) return 'unknown';
|
||||
|
||||
const hasVarArgs =
|
||||
def.parameterTypes !== undefined &&
|
||||
def.parameterTypes.some((t) => t === 'params' || t.startsWith('params '));
|
||||
|
||||
if (min !== undefined && argCount < min) return 'incompatible';
|
||||
if (max !== undefined && argCount > max && !hasVarArgs) return 'incompatible';
|
||||
|
||||
return 'compatible';
|
||||
}
|
||||
|
|
@ -17,11 +17,29 @@
|
|||
*/
|
||||
|
||||
import type { Capture, CaptureMatch } from 'gitnexus-shared';
|
||||
import { findNodeAtRange, nodeToCapture } from '../../utils/ast-helpers.js';
|
||||
import { findNodeAtRange, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
|
||||
import { splitUsingDirective } from './import-decomposer.js';
|
||||
import { computeCsharpArityMetadata } from './arity-metadata.js';
|
||||
import { getCsharpParser, getCsharpScopeQuery } from './query.js';
|
||||
import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
|
||||
|
||||
/** Declaration anchors that carry function-like arity metadata. */
|
||||
const FUNCTION_DECL_TAGS = [
|
||||
'@declaration.method',
|
||||
'@declaration.constructor',
|
||||
'@declaration.function',
|
||||
] as const;
|
||||
|
||||
/** tree-sitter-c-sharp node types that the method extractor accepts. */
|
||||
const FUNCTION_NODE_TYPES = [
|
||||
'method_declaration',
|
||||
'constructor_declaration',
|
||||
'destructor_declaration',
|
||||
'operator_declaration',
|
||||
'conversion_operator_declaration',
|
||||
'local_function_statement',
|
||||
] as const;
|
||||
|
||||
export function emitCsharpScopeCaptures(
|
||||
sourceText: string,
|
||||
_filePath: string,
|
||||
|
|
@ -72,8 +90,55 @@ export function emitCsharpScopeCaptures(
|
|||
continue;
|
||||
}
|
||||
|
||||
// Synthesize arity metadata on function-like declarations so the
|
||||
// registry can narrow overloads (C# relies heavily on this). Mirrors
|
||||
// Python's captures.ts pattern — one anchor per match, so we find
|
||||
// the first tag that matches.
|
||||
const declTag = FUNCTION_DECL_TAGS.find((t) => grouped[t] !== undefined);
|
||||
if (declTag !== undefined) {
|
||||
const anchor = grouped[declTag]!;
|
||||
const fnNode = findFunctionNode(tree.rootNode, anchor.range);
|
||||
if (fnNode !== null) {
|
||||
const arity = computeCsharpArityMetadata(fnNode);
|
||||
if (arity.parameterCount !== undefined) {
|
||||
grouped['@declaration.parameter-count'] = syntheticCapture(
|
||||
'@declaration.parameter-count',
|
||||
fnNode,
|
||||
String(arity.parameterCount),
|
||||
);
|
||||
}
|
||||
if (arity.requiredParameterCount !== undefined) {
|
||||
grouped['@declaration.required-parameter-count'] = syntheticCapture(
|
||||
'@declaration.required-parameter-count',
|
||||
fnNode,
|
||||
String(arity.requiredParameterCount),
|
||||
);
|
||||
}
|
||||
if (arity.parameterTypes !== undefined) {
|
||||
grouped['@declaration.parameter-types'] = syntheticCapture(
|
||||
'@declaration.parameter-types',
|
||||
fnNode,
|
||||
JSON.stringify(arity.parameterTypes),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
type SyntaxNode = ReturnType<ReturnType<typeof getCsharpParser>['parse']>['rootNode'];
|
||||
|
||||
/** Find the first C# function-like node at the given range. The
|
||||
* declaration anchor range covers the whole method/constructor/etc.
|
||||
* node, but the tag alone doesn't tell us which node type. */
|
||||
function findFunctionNode(rootNode: SyntaxNode, range: Capture['range']): SyntaxNode | null {
|
||||
for (const nodeType of FUNCTION_NODE_TYPES) {
|
||||
const n = findNodeAtRange(rootNode, range, nodeType);
|
||||
if (n !== null) return n as SyntaxNode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,6 +213,59 @@ describe('emitCsharpScopeCaptures — type bindings', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('emitCsharpScopeCaptures — arity metadata synthesis', () => {
|
||||
it('synthesizes parameter-count + required-parameter-count on method declarations', () => {
|
||||
const m = findMatch(
|
||||
'class A { public void M(int a, int b = 1) { } }',
|
||||
(t) =>
|
||||
t.includes('@declaration.method') &&
|
||||
t.includes('@declaration.parameter-count') &&
|
||||
t.includes('@declaration.required-parameter-count'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.parameter-count'].text).toBe('2');
|
||||
expect(m!['@declaration.required-parameter-count'].text).toBe('1');
|
||||
});
|
||||
|
||||
it('synthesizes parameter-types on method declarations', () => {
|
||||
const m = findMatch(
|
||||
'class A { public void M(User u, int n) { } }',
|
||||
(t) => t.includes('@declaration.method') && t.includes('@declaration.parameter-types'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
const types = JSON.parse(m!['@declaration.parameter-types'].text);
|
||||
expect(types).toEqual(['User', 'int']);
|
||||
});
|
||||
|
||||
it('leaves parameter-count undefined for `params` variadic methods', () => {
|
||||
const m = findMatch('class A { public void M(params int[] xs) { } }', (t) =>
|
||||
t.includes('@declaration.method'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.parameter-count']).toBeUndefined();
|
||||
expect(m!['@declaration.required-parameter-count']).toBeUndefined();
|
||||
const types = JSON.parse(m!['@declaration.parameter-types'].text);
|
||||
expect(types).toContain('params');
|
||||
});
|
||||
|
||||
it('synthesizes arity on constructor declarations', () => {
|
||||
const m = findMatch('class A { public A(int a, int b) { } }', (t) =>
|
||||
t.includes('@declaration.constructor'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.parameter-count'].text).toBe('2');
|
||||
expect(m!['@declaration.required-parameter-count'].text).toBe('2');
|
||||
});
|
||||
|
||||
it('synthesizes arity on local function declarations', () => {
|
||||
const m = findMatch('class A { void M() { void Local(int x) { } } }', (t) =>
|
||||
t.includes('@declaration.function'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.parameter-count'].text).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitCsharpScopeCaptures — references', () => {
|
||||
it('captures free call invocations', () => {
|
||||
const m = findMatch('class A { void M() { Foo(); } }', (t) =>
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ import {
|
|||
csharpReceiverBinding,
|
||||
} from '../../../../src/core/ingestion/languages/csharp/simple-hooks.js';
|
||||
import { csharpMergeBindings } from '../../../../src/core/ingestion/languages/csharp/merge-bindings.js';
|
||||
import { csharpArityCompatibility } from '../../../../src/core/ingestion/languages/csharp/arity.js';
|
||||
import type {
|
||||
BindingRef,
|
||||
Callsite,
|
||||
CaptureMatch,
|
||||
ParsedImport,
|
||||
Scope,
|
||||
|
|
@ -132,6 +134,62 @@ describe('csharpMergeBindings — shadowing precedence', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('csharpArityCompatibility', () => {
|
||||
const callsite = (arity: number): Callsite => ({ arity });
|
||||
const def = (o: Partial<SymbolDefinition> = {}): SymbolDefinition =>
|
||||
({ nodeId: 'd1', filePath: 't.cs', type: 'Function', ...o }) as SymbolDefinition;
|
||||
|
||||
it('unknown when both parameter counts are missing', () => {
|
||||
expect(csharpArityCompatibility(def(), callsite(2))).toBe('unknown');
|
||||
});
|
||||
|
||||
it('compatible inside [required, total]', () => {
|
||||
expect(
|
||||
csharpArityCompatibility(def({ parameterCount: 3, requiredParameterCount: 1 }), callsite(2)),
|
||||
).toBe('compatible');
|
||||
});
|
||||
|
||||
it('incompatible below required', () => {
|
||||
expect(
|
||||
csharpArityCompatibility(def({ parameterCount: 3, requiredParameterCount: 2 }), callsite(1)),
|
||||
).toBe('incompatible');
|
||||
});
|
||||
|
||||
it('incompatible above max without variadic', () => {
|
||||
expect(
|
||||
csharpArityCompatibility(def({ parameterCount: 2, requiredParameterCount: 0 }), callsite(5)),
|
||||
).toBe('incompatible');
|
||||
});
|
||||
|
||||
it('compatible above declared params when def has `params` variadic', () => {
|
||||
expect(
|
||||
csharpArityCompatibility(
|
||||
def({ parameterCount: undefined, requiredParameterCount: 0, parameterTypes: ['params'] }),
|
||||
callsite(7),
|
||||
),
|
||||
).toBe('compatible');
|
||||
});
|
||||
|
||||
it('compatible above declared params when variadic token prefixes', () => {
|
||||
expect(
|
||||
csharpArityCompatibility(
|
||||
def({
|
||||
parameterCount: undefined,
|
||||
requiredParameterCount: 1,
|
||||
parameterTypes: ['string', 'params int[]'],
|
||||
}),
|
||||
callsite(4),
|
||||
),
|
||||
).toBe('compatible');
|
||||
});
|
||||
|
||||
it('unknown for negative arity (defensive)', () => {
|
||||
expect(
|
||||
csharpArityCompatibility(def({ parameterCount: 3, requiredParameterCount: 1 }), callsite(-1)),
|
||||
).toBe('unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('csharpReceiverBinding', () => {
|
||||
it('returns the `this` type binding for an instance method scope', () => {
|
||||
const binding: TypeRef = { rawName: 'User', source: 'self' } as unknown as TypeRef;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue