mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-21 00:21:30 +00:00
feat(csharp-scope): unit 1 — scope query + captures orchestrator
First slice of the C# scope-resolution migration (issue #934, RFC #909 Ring 3). Closes `Unit 1` of docs/plans/2026-04-21-004-feat-csharp-scope-resolution-plan.md. Adds: - src/core/ingestion/languages/csharp/query.ts — tree-sitter scope query covering compilation_unit, namespace (block + file-scoped), class-like (class/interface/struct/record/enum), method-like (method/constructor/destructor/local_function/operator), property and field declarations, using directives, type bindings (parameter annotations, local variable annotations, constructor inference, invocation alias), and references (free call, member call including null-conditional, constructor call, member write). - src/core/ingestion/languages/csharp/captures.ts — pass-through orchestrator mirroring python/captures.ts. Import decomposition (Unit 2), receiver-type-binding synthesis (Unit 3), and arity metadata synthesis (Unit 5) stub out for future units. - src/core/ingestion/languages/csharp/cache-stats.ts — PROF instrumentation mirror of python/cache-stats.ts. Design notes: - Return-type / field-type / property-type captures deferred. tree-sitter-c-sharp does not expose these under a clean named field that pattern-matches. When Unit 7 parity gate surfaces a gap, add positional patterns or a post-hoc extractor lookup. - object_creation_expression with qualified_name type — the qualified name itself is the reference text; captured as a whole via a dedicated tag so interpretation in later units can split namespace + name. - Null-conditional calls use positional descendant patterns because tree-sitter-c-sharp's member_binding_expression and conditional_access_expression don't expose named fields. Coverage: - 23/23 new unit tests in test/unit/scope-resolution/csharp/csharp-captures.test.ts cover every capture tag. Confirmed against tree-sitter-c-sharp via the probe-script loop during development; grammar drift would surface as a capture-shape assertion failure. - tsc --noEmit clean. No changes to shared infrastructure. Resolver wiring + registration land in Unit 6.
This commit is contained in:
parent
ff4ae89aaa
commit
9cec7d16a2
4 changed files with 554 additions and 0 deletions
30
gitnexus/src/core/ingestion/languages/csharp/cache-stats.ts
Normal file
30
gitnexus/src/core/ingestion/languages/csharp/cache-stats.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Dev-mode counters for the cross-phase scope-captures parse cache
|
||||
* (C# mirror of `languages/python/cache-stats.ts`).
|
||||
*
|
||||
* Gated by `PROF_SCOPE_RESOLUTION=1`. Production builds fold every
|
||||
* increment into dead code via the module-level `PROF` constant, so
|
||||
* the hot path in `captures.ts` stays branch-free.
|
||||
*/
|
||||
|
||||
const PROF = process.env.PROF_SCOPE_RESOLUTION === '1';
|
||||
|
||||
let CACHE_HITS = 0;
|
||||
let CACHE_MISSES = 0;
|
||||
|
||||
export function recordCacheHit(): void {
|
||||
if (PROF) CACHE_HITS++;
|
||||
}
|
||||
|
||||
export function recordCacheMiss(): void {
|
||||
if (PROF) CACHE_MISSES++;
|
||||
}
|
||||
|
||||
export function getCsharpCaptureCacheStats(): { hits: number; misses: number } {
|
||||
return { hits: CACHE_HITS, misses: CACHE_MISSES };
|
||||
}
|
||||
|
||||
export function resetCsharpCaptureCacheStats(): void {
|
||||
CACHE_HITS = 0;
|
||||
CACHE_MISSES = 0;
|
||||
}
|
||||
54
gitnexus/src/core/ingestion/languages/csharp/captures.ts
Normal file
54
gitnexus/src/core/ingestion/languages/csharp/captures.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* `emitScopeCaptures` for C#.
|
||||
*
|
||||
* Drives the C# scope query against tree-sitter-c-sharp and groups
|
||||
* raw matches into `CaptureMatch[]` for the central extractor.
|
||||
*
|
||||
* Unit 1 shape: pure pass-through — each tree-sitter match becomes
|
||||
* one grouped `CaptureMatch`. Import decomposition (Unit 2),
|
||||
* receiver-type-binding synthesis (Unit 3), and arity metadata
|
||||
* synthesis (Unit 5) layer on top later.
|
||||
*
|
||||
* Pure given the input source text. No I/O, no globals consulted.
|
||||
*/
|
||||
|
||||
import type { Capture, CaptureMatch } from 'gitnexus-shared';
|
||||
import { nodeToCapture } from '../../utils/ast-helpers.js';
|
||||
import { getCsharpParser, getCsharpScopeQuery } from './query.js';
|
||||
import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
|
||||
|
||||
export function emitCsharpScopeCaptures(
|
||||
sourceText: string,
|
||||
_filePath: string,
|
||||
cachedTree?: unknown,
|
||||
): readonly CaptureMatch[] {
|
||||
// Skip the parse when the caller (parse phase's scopeTreeCache)
|
||||
// already produced a Tree for this source. Cache miss = re-parse,
|
||||
// same as before. The cachedTree parameter is typed as `unknown` at
|
||||
// the LanguageProvider contract layer; cast here at the use site.
|
||||
let tree = cachedTree as ReturnType<ReturnType<typeof getCsharpParser>['parse']> | undefined;
|
||||
if (tree === undefined) {
|
||||
tree = getCsharpParser().parse(sourceText);
|
||||
recordCacheMiss();
|
||||
} else {
|
||||
recordCacheHit();
|
||||
}
|
||||
|
||||
const rawMatches = getCsharpScopeQuery().matches(tree.rootNode);
|
||||
const out: CaptureMatch[] = [];
|
||||
|
||||
for (const m of rawMatches) {
|
||||
// Group captures by their tag name. Tree-sitter strips the leading
|
||||
// `@`; we put it back so the central extractor's prefix lookups
|
||||
// (`@scope.`, `@declaration.`, …) work.
|
||||
const grouped: Record<string, Capture> = {};
|
||||
for (const c of m.captures) {
|
||||
const tag = '@' + c.name;
|
||||
grouped[tag] = nodeToCapture(tag, c.node);
|
||||
}
|
||||
if (Object.keys(grouped).length === 0) continue;
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
244
gitnexus/src/core/ingestion/languages/csharp/query.ts
Normal file
244
gitnexus/src/core/ingestion/languages/csharp/query.ts
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
/**
|
||||
* Tree-sitter query for C# scope captures (RFC §5.1).
|
||||
*
|
||||
* Captures the structural skeleton the generic scope-resolution
|
||||
* pipeline consumes: scopes (module/namespace/class/function),
|
||||
* declarations (class-likes, method-likes, properties, variables,
|
||||
* local functions), imports (using directives), type bindings
|
||||
* (parameter annotations, variable annotations, constructor
|
||||
* inference), and references (call sites, member writes).
|
||||
*
|
||||
* C# specifics that shape this query:
|
||||
*
|
||||
* - Both block-scoped (`namespace X { }`) and file-scoped
|
||||
* (`namespace X;`) namespaces. tree-sitter-c-sharp emits them
|
||||
* under distinct node types (`namespace_declaration` vs
|
||||
* `file_scoped_namespace_declaration`); both map to
|
||||
* `@scope.namespace` since the scope semantics are identical.
|
||||
* - `partial class X` splits a Class def across files. Each file
|
||||
* emits its own `@declaration.class`; cross-file resolution is
|
||||
* handled at the graph-bridge layer via the qualified-name key.
|
||||
* - `using X = Y;` aliases and `using static X;` are interpreted in
|
||||
* `interpret.ts` via the `@import.*` captures. All three using
|
||||
* flavors share the same anchor (`@import.statement`).
|
||||
* - Explicit interface implementations (`void IFoo.Bar() { }`)
|
||||
* expose the qualified name via the existing `@declaration.name`
|
||||
* — the extractor's `csharpMethodConfig.extractQualifiedName`
|
||||
* picks up the explicit qualifier from the method declaration
|
||||
* node.
|
||||
*
|
||||
* Exposes lazy `Parser` and `Query` singletons so callers don't pay
|
||||
* tree-sitter init cost per file.
|
||||
*/
|
||||
|
||||
import Parser from 'tree-sitter';
|
||||
import CSharp from 'tree-sitter-c-sharp';
|
||||
|
||||
const CSHARP_SCOPE_QUERY = `
|
||||
;; Scopes
|
||||
(compilation_unit) @scope.module
|
||||
|
||||
(namespace_declaration) @scope.namespace
|
||||
(file_scoped_namespace_declaration) @scope.namespace
|
||||
|
||||
(class_declaration) @scope.class
|
||||
(interface_declaration) @scope.class
|
||||
(struct_declaration) @scope.class
|
||||
(record_declaration) @scope.class
|
||||
(enum_declaration) @scope.class
|
||||
|
||||
(method_declaration) @scope.function
|
||||
(constructor_declaration) @scope.function
|
||||
(destructor_declaration) @scope.function
|
||||
(local_function_statement) @scope.function
|
||||
(operator_declaration) @scope.function
|
||||
;; Property accessors are blocks within a property; not scoped here.
|
||||
;; Anonymous methods / lambdas are not scoped — out of scope per plan.
|
||||
|
||||
;; Declarations — types
|
||||
(class_declaration
|
||||
name: (identifier) @declaration.name) @declaration.class
|
||||
|
||||
(interface_declaration
|
||||
name: (identifier) @declaration.name) @declaration.interface
|
||||
|
||||
(struct_declaration
|
||||
name: (identifier) @declaration.name) @declaration.struct
|
||||
|
||||
(record_declaration
|
||||
name: (identifier) @declaration.name) @declaration.record
|
||||
|
||||
(enum_declaration
|
||||
name: (identifier) @declaration.name) @declaration.enum
|
||||
|
||||
;; Declarations — methods / constructors / properties
|
||||
(method_declaration
|
||||
name: (identifier) @declaration.name) @declaration.method
|
||||
|
||||
(constructor_declaration
|
||||
name: (identifier) @declaration.name) @declaration.constructor
|
||||
|
||||
(destructor_declaration
|
||||
name: (identifier) @declaration.name) @declaration.method
|
||||
|
||||
(local_function_statement
|
||||
name: (identifier) @declaration.name) @declaration.function
|
||||
|
||||
(property_declaration
|
||||
name: (identifier) @declaration.name) @declaration.property
|
||||
|
||||
(indexer_declaration) @declaration.property
|
||||
|
||||
;; Fields — \`int x;\` at class scope. variable_declarator inside
|
||||
;; field_declaration carries the name.
|
||||
(field_declaration
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name))) @declaration.variable
|
||||
|
||||
;; Local variables — \`int x = 1;\` inside a method body
|
||||
(local_declaration_statement
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @declaration.name))) @declaration.variable
|
||||
|
||||
;; Imports — single anchor per directive; interpretCsharpImport classifies
|
||||
(using_directive) @import.statement
|
||||
|
||||
;; Type bindings — parameter annotations: \`void F(User u)\`
|
||||
(parameter
|
||||
type: (identifier) @type-binding.type
|
||||
name: (identifier) @type-binding.name) @type-binding.parameter
|
||||
|
||||
(parameter
|
||||
type: (generic_name) @type-binding.type
|
||||
name: (identifier) @type-binding.name) @type-binding.parameter
|
||||
|
||||
(parameter
|
||||
type: (qualified_name) @type-binding.type
|
||||
name: (identifier) @type-binding.name) @type-binding.parameter
|
||||
|
||||
(parameter
|
||||
type: (nullable_type) @type-binding.type
|
||||
name: (identifier) @type-binding.name) @type-binding.parameter
|
||||
|
||||
;; Type bindings — local variable annotations: \`User u = new User();\`
|
||||
;; Typed local with identifier type + \`new X()\` initializer — shape
|
||||
;; matters so \`u\` binds to \`X\` (the constructor call's type), not the
|
||||
;; declared type alias (which is usually the same, but \`new DerivedUser()\`
|
||||
;; would be distinct).
|
||||
(local_declaration_statement
|
||||
(variable_declaration
|
||||
type: (identifier) @type-binding.type
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name))) @type-binding.annotation
|
||||
|
||||
(local_declaration_statement
|
||||
(variable_declaration
|
||||
type: (generic_name) @type-binding.type
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name))) @type-binding.annotation
|
||||
|
||||
(local_declaration_statement
|
||||
(variable_declaration
|
||||
type: (qualified_name) @type-binding.type
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name))) @type-binding.annotation
|
||||
|
||||
;; Type bindings — \`var u = new User();\` — constructor-inferred.
|
||||
;; Captures object_creation_expression's type as the binding type.
|
||||
;; variable_declarator wraps the \`= <expr>\` directly; tree-sitter-c-sharp
|
||||
;; does not surface an equals_value_clause wrapper here.
|
||||
(local_declaration_statement
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
(object_creation_expression
|
||||
type: (identifier) @type-binding.type)))) @type-binding.constructor
|
||||
|
||||
(local_declaration_statement
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
(object_creation_expression
|
||||
type: (generic_name) @type-binding.type)))) @type-binding.constructor
|
||||
|
||||
(local_declaration_statement
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
(object_creation_expression
|
||||
type: (qualified_name) @type-binding.type)))) @type-binding.constructor
|
||||
|
||||
;; Type bindings — \`var u = factory();\` alias (chain-follow picks up
|
||||
;; factory's return type via propagateImportedReturnTypes)
|
||||
(local_declaration_statement
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @type-binding.name
|
||||
(invocation_expression
|
||||
function: (identifier) @type-binding.type)))) @type-binding.alias
|
||||
|
||||
;; Return-type captures on method_declaration / property_declaration /
|
||||
;; field_declaration are deferred — tree-sitter-c-sharp does not expose
|
||||
;; the return/field type under a simple named field that pattern-matches
|
||||
;; cleanly. When Unit 7's parity gate surfaces a gap requiring these
|
||||
;; bindings, revisit with a positional pattern or a post-hoc lookup via
|
||||
;; csharpMethodConfig.extractReturnType / csharpFieldConfig.extractType.
|
||||
|
||||
;; References — free calls: \`Foo()\`
|
||||
(invocation_expression
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
||||
;; References — member calls: \`obj.Method()\`
|
||||
(invocation_expression
|
||||
function: (member_access_expression
|
||||
expression: (_) @reference.receiver
|
||||
name: (identifier) @reference.name)) @reference.call.member
|
||||
|
||||
;; References — null-conditional member calls: \`obj?.Method()\`
|
||||
;; Positional descendants — conditional_access_expression wraps a
|
||||
;; receiver followed by a member_binding_expression containing an
|
||||
;; identifier. tree-sitter-c-sharp doesn't expose named fields here.
|
||||
(invocation_expression
|
||||
function: (conditional_access_expression
|
||||
(member_binding_expression
|
||||
(identifier) @reference.name))) @reference.call.member
|
||||
|
||||
;; References — constructor calls: \`new User(...)\`
|
||||
(object_creation_expression
|
||||
type: (identifier) @reference.name) @reference.call.constructor
|
||||
|
||||
(object_creation_expression
|
||||
type: (generic_name
|
||||
(identifier) @reference.name)) @reference.call.constructor
|
||||
|
||||
(object_creation_expression
|
||||
type: (qualified_name) @reference.call.constructor.qualified) @reference.call.constructor
|
||||
|
||||
;; References — field/property writes: \`obj.Name = "x"\` emits a write
|
||||
;; ACCESSES edge from the enclosing method to the field/property on
|
||||
;; obj's class.
|
||||
(assignment_expression
|
||||
left: (member_access_expression
|
||||
expression: (_) @reference.receiver
|
||||
name: (identifier) @reference.name)) @reference.write.member
|
||||
`;
|
||||
|
||||
let _parser: Parser | null = null;
|
||||
let _query: Parser.Query | null = null;
|
||||
|
||||
export function getCsharpParser(): Parser {
|
||||
if (_parser === null) {
|
||||
_parser = new Parser();
|
||||
_parser.setLanguage(CSharp as Parameters<Parser['setLanguage']>[0]);
|
||||
}
|
||||
return _parser;
|
||||
}
|
||||
|
||||
export function getCsharpScopeQuery(): Parser.Query {
|
||||
if (_query === null) {
|
||||
_query = new Parser.Query(CSharp as Parameters<Parser['setLanguage']>[0], CSHARP_SCOPE_QUERY);
|
||||
}
|
||||
return _query;
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
/**
|
||||
* Unit 1 coverage for the C# scope query + captures orchestrator.
|
||||
*
|
||||
* Pins the capture-tag vocabulary + range shape for every construct
|
||||
* the scope-resolution pipeline reads. Runs against tree-sitter-c-sharp
|
||||
* so it catches grammar drift (node renames, field-name changes)
|
||||
* before the integration parity gate does.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { emitCsharpScopeCaptures } from '../../../../src/core/ingestion/languages/csharp/captures.js';
|
||||
|
||||
function tagsFor(src: string): string[][] {
|
||||
const matches = emitCsharpScopeCaptures(src, 'test.cs');
|
||||
return matches.map((m) => Object.keys(m).sort());
|
||||
}
|
||||
|
||||
function findMatch(src: string, predicate: (tags: string[]) => boolean) {
|
||||
const matches = emitCsharpScopeCaptures(src, 'test.cs');
|
||||
return matches.find((m) => predicate(Object.keys(m)));
|
||||
}
|
||||
|
||||
describe('emitCsharpScopeCaptures — scopes', () => {
|
||||
it('captures the compilation unit as @scope.module', () => {
|
||||
const all = tagsFor('class A { }');
|
||||
expect(all.some((t) => t.includes('@scope.module'))).toBe(true);
|
||||
});
|
||||
|
||||
it('captures block-scoped namespaces as @scope.namespace', () => {
|
||||
const all = tagsFor('namespace Foo.Bar { class A { } }');
|
||||
expect(all.some((t) => t.includes('@scope.namespace'))).toBe(true);
|
||||
});
|
||||
|
||||
it('captures file-scoped namespaces as @scope.namespace', () => {
|
||||
const all = tagsFor('namespace Foo.Bar;\nclass A { }');
|
||||
expect(all.some((t) => t.includes('@scope.namespace'))).toBe(true);
|
||||
});
|
||||
|
||||
it('captures classes, interfaces, structs, records, enums as @scope.class', () => {
|
||||
// All four class-like kinds collapse to @scope.class at the scope
|
||||
// layer because they share the same scope semantics (body is a
|
||||
// member-holding scope). Declaration tags distinguish them.
|
||||
const src = `
|
||||
class A { }
|
||||
interface B { }
|
||||
struct C { }
|
||||
record D(int x);
|
||||
enum E { V1, V2 }
|
||||
`;
|
||||
const all = tagsFor(src);
|
||||
const scopeClassCount = all.filter((t) => t.includes('@scope.class')).length;
|
||||
expect(scopeClassCount).toBe(5);
|
||||
});
|
||||
|
||||
it('captures methods, constructors, destructors, local functions as @scope.function', () => {
|
||||
const src = `
|
||||
class A {
|
||||
public A() { }
|
||||
~A() { }
|
||||
public void M() {
|
||||
void Local() { }
|
||||
}
|
||||
}
|
||||
`;
|
||||
const all = tagsFor(src);
|
||||
const scopeFnCount = all.filter((t) => t.includes('@scope.function')).length;
|
||||
expect(scopeFnCount).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitCsharpScopeCaptures — declarations', () => {
|
||||
it('captures class declarations with @declaration.class + @declaration.name', () => {
|
||||
const m = findMatch('class User { }', (t) => t.includes('@declaration.class'));
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.name'].text).toBe('User');
|
||||
});
|
||||
|
||||
it('captures interface declarations distinctly from class declarations', () => {
|
||||
const m = findMatch('interface IUser { }', (t) => t.includes('@declaration.interface'));
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.name'].text).toBe('IUser');
|
||||
});
|
||||
|
||||
it('captures struct, record, enum with their own declaration tags', () => {
|
||||
expect(findMatch('struct Point { }', (t) => t.includes('@declaration.struct'))).toBeDefined();
|
||||
expect(findMatch('record R(int x);', (t) => t.includes('@declaration.record'))).toBeDefined();
|
||||
expect(findMatch('enum E { V }', (t) => t.includes('@declaration.enum'))).toBeDefined();
|
||||
});
|
||||
|
||||
it('captures method declarations with their name', () => {
|
||||
const m = findMatch('class A { public void Save() { } }', (t) =>
|
||||
t.includes('@declaration.method'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.name'].text).toBe('Save');
|
||||
});
|
||||
|
||||
it('captures constructor declarations under @declaration.constructor', () => {
|
||||
const m = findMatch('class A { public A() { } }', (t) =>
|
||||
t.includes('@declaration.constructor'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.name'].text).toBe('A');
|
||||
});
|
||||
|
||||
it('captures property declarations', () => {
|
||||
const m = findMatch('class A { public int Age { get; set; } }', (t) =>
|
||||
t.includes('@declaration.property'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.name'].text).toBe('Age');
|
||||
});
|
||||
|
||||
it('captures field declarations as @declaration.variable', () => {
|
||||
const m = findMatch('class A { private int _x; }', (t) => t.includes('@declaration.variable'));
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.name'].text).toBe('_x');
|
||||
});
|
||||
|
||||
it('captures local function declarations', () => {
|
||||
const m = findMatch('class A { void M() { void Local() { } } }', (t) =>
|
||||
t.includes('@declaration.function'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@declaration.name'].text).toBe('Local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitCsharpScopeCaptures — imports', () => {
|
||||
it('captures each `using` directive as @import.statement', () => {
|
||||
const src = `
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dict = System.Collections.Generic.Dictionary<string, int>;
|
||||
using static System.Math;
|
||||
`;
|
||||
const all = tagsFor(src);
|
||||
const importCount = all.filter((t) => t.includes('@import.statement')).length;
|
||||
expect(importCount).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitCsharpScopeCaptures — type bindings', () => {
|
||||
it('captures parameter annotations (object types)', () => {
|
||||
// `int id` does NOT fire (predefined_type is not identifier) —
|
||||
// only object-type parameters do. That's intentional: receiver-
|
||||
// bound dispatch doesn't need primitives.
|
||||
const m = findMatch('class A { void M(User u) { } }', (t) =>
|
||||
t.includes('@type-binding.parameter'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@type-binding.name'].text).toBe('u');
|
||||
expect(m!['@type-binding.type'].text).toBe('User');
|
||||
});
|
||||
|
||||
it('captures local variable annotations', () => {
|
||||
const m = findMatch('class A { void M() { User u; } }', (t) =>
|
||||
t.includes('@type-binding.annotation'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@type-binding.name'].text).toBe('u');
|
||||
expect(m!['@type-binding.type'].text).toBe('User');
|
||||
});
|
||||
|
||||
it('captures constructor-inferred `var u = new User();`', () => {
|
||||
const m = findMatch('class A { void M() { var u = new User(); } }', (t) =>
|
||||
t.includes('@type-binding.constructor'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@type-binding.name'].text).toBe('u');
|
||||
expect(m!['@type-binding.type'].text).toBe('User');
|
||||
});
|
||||
|
||||
it('captures alias `var u = Factory();`', () => {
|
||||
const m = findMatch('class A { void M() { var u = Factory(); } }', (t) =>
|
||||
t.includes('@type-binding.alias'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@type-binding.name'].text).toBe('u');
|
||||
expect(m!['@type-binding.type'].text).toBe('Factory');
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitCsharpScopeCaptures — references', () => {
|
||||
it('captures free call invocations', () => {
|
||||
const m = findMatch('class A { void M() { Foo(); } }', (t) =>
|
||||
t.includes('@reference.call.free'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@reference.name'].text).toBe('Foo');
|
||||
});
|
||||
|
||||
it('captures member call invocations with receiver + name', () => {
|
||||
const m = findMatch('class A { void M() { obj.Save(); } }', (t) =>
|
||||
t.includes('@reference.call.member'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@reference.receiver'].text).toBe('obj');
|
||||
expect(m!['@reference.name'].text).toBe('Save');
|
||||
});
|
||||
|
||||
it('captures null-conditional member calls `obj?.Save()`', () => {
|
||||
const m = findMatch('class A { void M(User obj) { obj?.Save(); } }', (t) =>
|
||||
t.includes('@reference.call.member'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@reference.name'].text).toBe('Save');
|
||||
});
|
||||
|
||||
it('captures object-creation expressions as constructor calls', () => {
|
||||
const m = findMatch('class A { void M() { var u = new User(); } }', (t) =>
|
||||
t.includes('@reference.call.constructor'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@reference.name'].text).toBe('User');
|
||||
});
|
||||
|
||||
it('captures member writes `obj.Name = "x"`', () => {
|
||||
const m = findMatch('class A { void M(User obj) { obj.Name = "x"; } }', (t) =>
|
||||
t.includes('@reference.write.member'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@reference.receiver'].text).toBe('obj');
|
||||
expect(m!['@reference.name'].text).toBe('Name');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue