mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-17 23:52:36 +00:00
Merge branch 'main' into feat/cpp-ue-macro-preprocessor
This commit is contained in:
commit
0ee01bcaa3
13 changed files with 408 additions and 23 deletions
|
|
@ -9,6 +9,11 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|||
|
||||
const CSHARP_VIS = new Set<FieldVisibility>(['public', 'private', 'protected', 'internal']);
|
||||
|
||||
const extractCsharpDeclaredType = (typeNode: SyntaxNode): string | undefined => {
|
||||
if (typeNode.type === 'generic_name') return typeNode.text.trim();
|
||||
return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* C# field extraction config.
|
||||
*
|
||||
|
|
@ -53,17 +58,17 @@ export const csharpConfig: FieldExtractionConfig = {
|
|||
const child = node.namedChild(i);
|
||||
if (child?.type === 'variable_declaration') {
|
||||
const typeNode = child.childForFieldName('type');
|
||||
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
|
||||
if (typeNode) return extractCsharpDeclaredType(typeNode);
|
||||
// fallback: first child that is a type
|
||||
const first = child.firstNamedChild;
|
||||
if (first && first.type !== 'variable_declarator') {
|
||||
return extractSimpleTypeName(first) ?? first.text?.trim();
|
||||
return extractCsharpDeclaredType(first);
|
||||
}
|
||||
}
|
||||
}
|
||||
// property_declaration: type is first named child
|
||||
const typeNode = node.childForFieldName('type');
|
||||
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
|
||||
if (typeNode) return extractCsharpDeclaredType(typeNode);
|
||||
return undefined;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,39 @@ const FUNCTION_NODE_TYPES = [
|
|||
'local_function_statement',
|
||||
] as const;
|
||||
|
||||
const BUILTIN_TYPE_NAMES = new Set([
|
||||
'bool',
|
||||
'byte',
|
||||
'char',
|
||||
'decimal',
|
||||
'double',
|
||||
'float',
|
||||
'int',
|
||||
'long',
|
||||
'object',
|
||||
'sbyte',
|
||||
'short',
|
||||
'string',
|
||||
'uint',
|
||||
'ulong',
|
||||
'ushort',
|
||||
'void',
|
||||
]);
|
||||
|
||||
function shouldEmitReadMember(memberNode: SyntaxNode): boolean {
|
||||
const parent = memberNode.parent;
|
||||
if (parent === null) return true;
|
||||
|
||||
switch (parent.type) {
|
||||
case 'invocation_expression':
|
||||
return parent.childForFieldName('function')?.id !== memberNode.id;
|
||||
case 'assignment_expression':
|
||||
return parent.childForFieldName('left')?.id !== memberNode.id;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function emitCsharpScopeCaptures(
|
||||
sourceText: string,
|
||||
_filePath: string,
|
||||
|
|
@ -94,6 +127,14 @@ export function emitCsharpScopeCaptures(
|
|||
continue;
|
||||
}
|
||||
|
||||
if (grouped['@reference.read.member'] !== undefined) {
|
||||
const anchor = grouped['@reference.read.member'];
|
||||
const memberNode = findNodeAtRange(tree.rootNode, anchor.range, 'member_access_expression');
|
||||
if (memberNode === null || !shouldEmitReadMember(memberNode)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Synthesize `this` / `base` receiver type-bindings on every
|
||||
// instance method-like. Tree-sitter can't cleanly express "the
|
||||
// implicit receiver of a non-static member of a class/struct/
|
||||
|
|
@ -209,9 +250,63 @@ export function emitCsharpScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
out.push(...synthesizeGenericTypeArgumentReferences(tree.rootNode));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function synthesizeGenericTypeArgumentReferences(root: SyntaxNode): CaptureMatch[] {
|
||||
const out: CaptureMatch[] = [];
|
||||
// Treat all generic type arguments as static type references, including
|
||||
// declaration signatures and call-site generic instantiations.
|
||||
visit(root, (node) => {
|
||||
if (node.type !== 'generic_name') return;
|
||||
const args = findNamedChild(node, 'type_argument_list');
|
||||
if (args === null) return;
|
||||
|
||||
for (const arg of args.namedChildren) {
|
||||
if (arg === null) continue;
|
||||
const nameNode = terminalTypeNameNode(arg);
|
||||
if (nameNode === null) continue;
|
||||
if (BUILTIN_TYPE_NAMES.has(nameNode.text)) continue;
|
||||
out.push({
|
||||
'@reference.type': nodeToCapture('@reference.type', nameNode),
|
||||
'@reference.name': nodeToCapture('@reference.name', nameNode),
|
||||
});
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function terminalTypeNameNode(node: SyntaxNode): SyntaxNode | null {
|
||||
switch (node.type) {
|
||||
case 'identifier':
|
||||
return node;
|
||||
case 'nullable_type':
|
||||
return node.firstNamedChild === null ? null : terminalTypeNameNode(node.firstNamedChild);
|
||||
case 'qualified_name':
|
||||
return node.lastNamedChild;
|
||||
case 'generic_name':
|
||||
return node.childForFieldName('name') ?? node.firstNamedChild;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function findNamedChild(node: SyntaxNode, type: string): SyntaxNode | null {
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null && child.type === type) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function visit(node: SyntaxNode, cb: (node: SyntaxNode) => void): void {
|
||||
cb(node);
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) visit(child, cb);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
|
|
|||
|
|
@ -499,6 +499,12 @@ const CSHARP_SCOPE_QUERY = `
|
|||
left: (member_access_expression
|
||||
expression: "base" @reference.receiver
|
||||
name: (identifier) @reference.name)) @reference.write.member
|
||||
|
||||
;; References — field/property reads: \`obj.Name\`
|
||||
;; Emit-side filtering drops call targets and assignment left-hand sides.
|
||||
(member_access_expression
|
||||
expression: (_) @reference.receiver
|
||||
name: (identifier) @reference.name) @reference.read.member
|
||||
`;
|
||||
|
||||
let _parser: Parser | null = null;
|
||||
|
|
|
|||
|
|
@ -301,11 +301,15 @@ export const streamAllCSVsToDisk = async (
|
|||
'Template',
|
||||
'Module',
|
||||
] as const;
|
||||
const propertyHeader = 'id,name,filePath,startLine,endLine,content,description,declaredType';
|
||||
const multiLangWriters = new Map<string, BufferedCSVWriter>();
|
||||
for (const t of MULTI_LANG_TYPES) {
|
||||
multiLangWriters.set(
|
||||
t,
|
||||
new BufferedCSVWriter(path.join(csvDir, `${t.toLowerCase()}.csv`), multiLangHeader),
|
||||
new BufferedCSVWriter(
|
||||
path.join(csvDir, `${t.toLowerCase()}.csv`),
|
||||
t === 'Property' ? propertyHeader : multiLangHeader,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -478,6 +482,9 @@ export const streamAllCSVsToDisk = async (
|
|||
escapeCSVNumber(node.properties.endLine, -1),
|
||||
escapeCSVField(content),
|
||||
escapeCSVField(node.properties.description || ''),
|
||||
...(node.label === 'Property'
|
||||
? [escapeCSVField(node.properties.declaredType || '')]
|
||||
: []),
|
||||
].join(','),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -608,6 +608,9 @@ const getCopyQuery = (table: NodeTableName, filePath: string): string => {
|
|||
if (table === 'Method') {
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, parameterCount, returnType) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
}
|
||||
if (table === 'Property') {
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, content, description, declaredType) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
}
|
||||
// TypeScript/JS code element tables have isExported; multi-language tables do not
|
||||
if (TABLES_WITH_EXPORTED.has(table)) {
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
|
|
@ -659,6 +662,11 @@ export const insertNodeToLbug = async (
|
|||
? `, description: ${escapeValue(properties.description)}`
|
||||
: '';
|
||||
query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${escapeValue(properties.content || '')}${descPart}})`;
|
||||
} else if (label === 'Property') {
|
||||
const descPart = properties.description
|
||||
? `, description: ${escapeValue(properties.description)}`
|
||||
: '';
|
||||
query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}${descPart}, declaredType: ${escapeValue(properties.declaredType || '')}})`;
|
||||
} else {
|
||||
// Multi-language tables (Struct, Impl, Trait, Macro, etc.) — no isExported
|
||||
const descPart = properties.description
|
||||
|
|
@ -737,6 +745,11 @@ export const batchInsertNodesToLbug = async (
|
|||
? `, n.description = ${escapeValue(properties.description)}`
|
||||
: '';
|
||||
query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
|
||||
} else if (label === 'Property') {
|
||||
const descPart = properties.description
|
||||
? `, n.description = ${escapeValue(properties.description)}`
|
||||
: '';
|
||||
query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${escapeValue(properties.content || '')}${descPart}, n.declaredType = ${escapeValue(properties.declaredType || '')}`;
|
||||
} else {
|
||||
const descPart = properties.description
|
||||
? `, n.description = ${escapeValue(properties.description)}`
|
||||
|
|
|
|||
|
|
@ -167,7 +167,18 @@ export const TYPE_ALIAS_SCHEMA = CODE_ELEMENT_BASE('TypeAlias');
|
|||
export const CONST_SCHEMA = CODE_ELEMENT_BASE('Const');
|
||||
export const STATIC_SCHEMA = CODE_ELEMENT_BASE('Static');
|
||||
export const VARIABLE_SCHEMA = CODE_ELEMENT_BASE('Variable');
|
||||
export const PROPERTY_SCHEMA = CODE_ELEMENT_BASE('Property');
|
||||
export const PROPERTY_SCHEMA = `
|
||||
CREATE NODE TABLE \`Property\` (
|
||||
id STRING,
|
||||
name STRING,
|
||||
filePath STRING,
|
||||
startLine INT64,
|
||||
endLine INT64,
|
||||
content STRING,
|
||||
description STRING,
|
||||
declaredType STRING,
|
||||
PRIMARY KEY (id)
|
||||
)`;
|
||||
export const RECORD_SCHEMA = CODE_ELEMENT_BASE('Record');
|
||||
export const DELEGATE_SCHEMA = CODE_ELEMENT_BASE('Delegate');
|
||||
export const ANNOTATION_SCHEMA = CODE_ELEMENT_BASE('Annotation');
|
||||
|
|
|
|||
|
|
@ -1756,12 +1756,13 @@ export class LocalBackend {
|
|||
repo.id,
|
||||
`
|
||||
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
|
||||
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
||||
LIMIT 30
|
||||
`,
|
||||
{ symId },
|
||||
);
|
||||
let typedPropertyRows: any[] = [];
|
||||
|
||||
// Fix #480: Class/Interface nodes have no direct CALLS/IMPORTS edges —
|
||||
// those point to Constructor and File nodes respectively. Fetch those
|
||||
|
|
@ -1795,23 +1796,24 @@ export class LocalBackend {
|
|||
|
||||
if (isClassLike) {
|
||||
try {
|
||||
// Run both incoming-ref queries in parallel — they are independent.
|
||||
const [ctorIncoming, fileIncoming] = await Promise.all([
|
||||
executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
// Run incoming-ref queries in parallel — they are independent.
|
||||
const [ctorIncoming, fileIncoming, typedPropertyIncoming, typedProperties] =
|
||||
await Promise.all([
|
||||
executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
MATCH (n)-[hm:CodeRelation]->(ctor:Constructor)
|
||||
WHERE n.id = $symId AND hm.type = 'HAS_METHOD'
|
||||
MATCH (caller)-[r:CodeRelation]->(ctor)
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'ACCESSES']
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'ACCESSES']
|
||||
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
||||
LIMIT 30
|
||||
`,
|
||||
{ symId },
|
||||
),
|
||||
executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
{ symId },
|
||||
),
|
||||
executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
MATCH (f:File)-[rel:CodeRelation]->(n)
|
||||
WHERE n.id = $symId AND rel.type = 'DEFINES'
|
||||
MATCH (caller)-[r:CodeRelation]->(f)
|
||||
|
|
@ -1819,9 +1821,45 @@ export class LocalBackend {
|
|||
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
||||
LIMIT 30
|
||||
`,
|
||||
{ symId },
|
||||
),
|
||||
]);
|
||||
{ symId },
|
||||
),
|
||||
executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
MATCH (p:\`Property\`)
|
||||
WHERE p.declaredType = $name
|
||||
OR p.declaredType STARTS WITH $genericPrefix
|
||||
OR p.declaredType CONTAINS $genericArg
|
||||
MATCH (caller)-[r:CodeRelation]->(p)
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'ACCESSES']
|
||||
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
||||
LIMIT 30
|
||||
`,
|
||||
{
|
||||
name: sym.name,
|
||||
genericPrefix: `${sym.name}<`,
|
||||
genericArg: `<${sym.name}>`,
|
||||
},
|
||||
),
|
||||
executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
MATCH (p:\`Property\`)
|
||||
WHERE p.declaredType = $name
|
||||
OR p.declaredType STARTS WITH $genericPrefix
|
||||
OR p.declaredType CONTAINS $genericArg
|
||||
RETURN p.id AS uid, p.name AS name, p.filePath AS filePath, labels(p)[0] AS kind,
|
||||
p.declaredType AS declaredType
|
||||
LIMIT 30
|
||||
`,
|
||||
{
|
||||
name: sym.name,
|
||||
genericPrefix: `${sym.name}<`,
|
||||
genericArg: `<${sym.name}>`,
|
||||
},
|
||||
),
|
||||
]);
|
||||
typedPropertyRows = typedProperties;
|
||||
|
||||
// Deduplicate by (relType, uid) — a caller can have multiple relation
|
||||
// types to the same target (e.g. both IMPORTS and CALLS), and each
|
||||
|
|
@ -1829,7 +1867,7 @@ export class LocalBackend {
|
|||
const seenKeys = new Set(
|
||||
incomingRows.map((r: any) => `${r.relType || r[0]}:${r.uid || r[1]}`),
|
||||
);
|
||||
for (const r of [...ctorIncoming, ...fileIncoming]) {
|
||||
for (const r of [...ctorIncoming, ...fileIncoming, ...typedPropertyIncoming]) {
|
||||
const key = `${r.relType || r[0]}:${r.uid || r[1]}`;
|
||||
if (!seenKeys.has(key)) {
|
||||
seenKeys.add(key);
|
||||
|
|
@ -1846,7 +1884,7 @@ export class LocalBackend {
|
|||
repo.id,
|
||||
`
|
||||
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
|
||||
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
|
||||
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
||||
LIMIT 30
|
||||
`,
|
||||
|
|
@ -1935,6 +1973,17 @@ export class LocalBackend {
|
|||
},
|
||||
incoming: categorize(incomingRows),
|
||||
outgoing: categorize(outgoingRows),
|
||||
...(typedPropertyRows.length > 0
|
||||
? {
|
||||
typed_properties: typedPropertyRows.map((r: any) => ({
|
||||
uid: r.uid || r[0],
|
||||
name: r.name || r[1],
|
||||
filePath: r.filePath || r[2],
|
||||
kind: r.kind || r[3],
|
||||
declaredType: r.declaredType || r[4],
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
processes: processRows.map((r: any) => ({
|
||||
id: r.pid || r[0],
|
||||
name: r.label || r[1],
|
||||
|
|
@ -2500,6 +2549,7 @@ export class LocalBackend {
|
|||
const mappedRelTypes = params.relationTypes?.flatMap((t: string) =>
|
||||
t === 'OVERRIDES' ? ['OVERRIDES', 'METHOD_OVERRIDES'] : [t],
|
||||
);
|
||||
const hasExplicitRelationTypes = mappedRelTypes !== undefined && mappedRelTypes.length > 0;
|
||||
const rawRelTypes =
|
||||
mappedRelTypes && mappedRelTypes.length > 0
|
||||
? mappedRelTypes.filter((t: string) => VALID_RELATION_TYPES.has(t))
|
||||
|
|
@ -2508,6 +2558,7 @@ export class LocalBackend {
|
|||
'IMPORTS',
|
||||
'EXTENDS',
|
||||
'IMPLEMENTS',
|
||||
'USES',
|
||||
'METHOD_OVERRIDES',
|
||||
'OVERRIDES',
|
||||
'METHOD_IMPLEMENTS',
|
||||
|
|
@ -2520,6 +2571,7 @@ export class LocalBackend {
|
|||
'IMPORTS',
|
||||
'EXTENDS',
|
||||
'IMPLEMENTS',
|
||||
'USES',
|
||||
'METHOD_OVERRIDES',
|
||||
'OVERRIDES',
|
||||
'METHOD_IMPLEMENTS',
|
||||
|
|
@ -2579,9 +2631,16 @@ export class LocalBackend {
|
|||
};
|
||||
const symType = outcome.resolvedLabel || outcome.symbol.type || '';
|
||||
|
||||
const effectiveRelationTypes =
|
||||
(symType === 'Class' || symType === 'Interface') &&
|
||||
!hasExplicitRelationTypes &&
|
||||
!relationTypes.includes('ACCESSES')
|
||||
? [...relationTypes, 'ACCESSES']
|
||||
: relationTypes;
|
||||
|
||||
return this._runImpactBFS(repo, sym, symType, direction, {
|
||||
maxDepth,
|
||||
relationTypes,
|
||||
relationTypes: effectiveRelationTypes,
|
||||
includeTests,
|
||||
minConfidence,
|
||||
});
|
||||
|
|
@ -2660,6 +2719,30 @@ export class LocalBackend {
|
|||
frontier.push(rid);
|
||||
}
|
||||
}
|
||||
|
||||
const typedPropertyRows = await executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
MATCH (p:\`Property\`)
|
||||
WHERE p.declaredType = $name
|
||||
OR p.declaredType STARTS WITH $genericPrefix
|
||||
OR p.declaredType CONTAINS $genericArg
|
||||
RETURN p.id AS id, p.name AS name, labels(p)[0] AS type, p.filePath AS filePath
|
||||
`,
|
||||
{
|
||||
name: sym.name,
|
||||
genericPrefix: `${sym.name}<`,
|
||||
genericArg: `<${sym.name}>`,
|
||||
},
|
||||
);
|
||||
|
||||
for (const r of typedPropertyRows) {
|
||||
const rid = r.id || r[0];
|
||||
if (rid && !visited.has(rid)) {
|
||||
visited.add(rid);
|
||||
frontier.push(rid);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logQueryError('impact:class-node-expansion', e);
|
||||
}
|
||||
|
|
|
|||
18
gitnexus/test/fixtures/lang-resolution/csharp-generic-type-refs/Program.cs
vendored
Normal file
18
gitnexus/test/fixtures/lang-resolution/csharp-generic-type-refs/Program.cs
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace App;
|
||||
|
||||
public class USER_INFO
|
||||
{
|
||||
public string? USER_ID { get; set; }
|
||||
}
|
||||
|
||||
public interface IEntityTypeConfiguration<T>
|
||||
{
|
||||
}
|
||||
|
||||
public class UserInfoConfiguration : IEntityTypeConfiguration<USER_INFO>
|
||||
{
|
||||
public Task<List<USER_INFO>> Load(List<USER_INFO> users)
|
||||
{
|
||||
return Task.FromResult(users);
|
||||
}
|
||||
}
|
||||
77
gitnexus/test/integration/context-typed-property.test.ts
Normal file
77
gitnexus/test/integration/context-typed-property.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* Integration test: context() expands Class symbols through typed properties.
|
||||
*
|
||||
* Reproduces EF-style usage where code reads a DbContext property
|
||||
* (`db.USER_INFO`) whose source type is `DbSet<USER_INFO>`. The direct
|
||||
* graph edge is Method -> Property, not Method -> Class, so context() must
|
||||
* use the same typed-property bridge that impact() uses.
|
||||
*/
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
||||
import { listRegisteredRepos } from '../../src/storage/repo-manager.js';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
|
||||
vi.mock('../../src/storage/repo-manager.js', () => ({
|
||||
listRegisteredRepos: vi.fn().mockResolvedValue([]),
|
||||
cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }),
|
||||
findSiblingClones: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
const SEED = [
|
||||
`CREATE (c:Class {id:'Class:Models/USER_INFO.cs:USER_INFO', name:'USER_INFO', filePath:'Models/USER_INFO.cs', startLine:1, endLine:5, content:'public class USER_INFO {}', description:''})`,
|
||||
`CREATE (p:\`Property\` {id:'Property:Data/UserDbContext.cs:UserDbContext.USER_INFO', name:'USER_INFO', filePath:'Data/UserDbContext.cs', startLine:10, endLine:10, content:'public DbSet<USER_INFO> USER_INFO { get; set; }', description:'', declaredType:'DbSet<USER_INFO>'})`,
|
||||
`CREATE (m:Method {id:'Method:Services/UserService.cs:UserService.GetUserInfo#1', name:'GetUserInfo', filePath:'Services/UserService.cs', startLine:20, endLine:30, isExported:false, content:'db.USER_INFO.FirstOrDefault();', description:'', parameterCount:1, returnType:'USER_INFO'})`,
|
||||
`MATCH (m:Method {id:'Method:Services/UserService.cs:UserService.GetUserInfo#1'}), (p:\`Property\` {id:'Property:Data/UserDbContext.cs:UserDbContext.USER_INFO'}) CREATE (m)-[:CodeRelation {type:'ACCESSES', confidence:1.0, reason:'read', step:1}]->(p)`,
|
||||
];
|
||||
|
||||
withTestLbugDB(
|
||||
'context-typed-property',
|
||||
(handle) => {
|
||||
let backend: LocalBackend;
|
||||
|
||||
beforeAll(async () => {
|
||||
backend = (handle as any)._backend;
|
||||
});
|
||||
|
||||
describe('context() typed-property expansion', () => {
|
||||
it('surfaces property callers and explains the typed property bridge', async () => {
|
||||
const result = await backend.callTool('context', {
|
||||
uid: 'Class:Models/USER_INFO.cs:USER_INFO',
|
||||
});
|
||||
|
||||
expect(result.status).toBe('found');
|
||||
expect(result.symbol.kind).toBe('Class');
|
||||
|
||||
const accesses = result.incoming.accesses || [];
|
||||
expect(accesses.map((r: any) => r.name)).toContain('GetUserInfo');
|
||||
|
||||
expect(result.typed_properties).toEqual([
|
||||
expect.objectContaining({
|
||||
uid: 'Property:Data/UserDbContext.cs:UserDbContext.USER_INFO',
|
||||
name: 'USER_INFO',
|
||||
declaredType: 'DbSet<USER_INFO>',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
seed: SEED,
|
||||
poolAdapter: true,
|
||||
afterSetup: async (handle) => {
|
||||
vi.mocked(listRegisteredRepos).mockResolvedValue([
|
||||
{
|
||||
name: 'test-repo',
|
||||
path: '/test/repo',
|
||||
storagePath: handle.tmpHandle.dbPath,
|
||||
indexedAt: new Date().toISOString(),
|
||||
lastCommit: 'abc123',
|
||||
stats: { files: 3, nodes: 3, communities: 0, processes: 0 },
|
||||
},
|
||||
]);
|
||||
const backend = new LocalBackend();
|
||||
await backend.init();
|
||||
(handle as any)._backend = backend;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -1440,6 +1440,24 @@ describe('Write access tracking (C#)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generic type references: IEntityTypeConfiguration<USER_INFO>, List<USER_INFO>
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# generic type-reference tracking', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-generic-type-refs'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('emits USES edges for generic type arguments', () => {
|
||||
const uses = getRelationships(result, 'USES').filter((e) => e.target === 'USER_INFO');
|
||||
expect(edgeSet(uses)).toContain('UserInfoConfiguration → USER_INFO');
|
||||
expect(edgeSet(uses)).toContain('Load → USER_INFO');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call-result variable binding (Phase 9): var user = GetUser(); user.Save()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import type { GraphRelationship } from 'gitnexus-shared';
|
|||
const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, ReadonlySet<string>>> = {
|
||||
csharp: new Set([
|
||||
'emits the using-import edge App/Program.cs -> Models/User.cs through the scope-resolution path',
|
||||
// Generic type-argument USES edges are emitted by the registry-primary
|
||||
// resolver only; the legacy DAG path does not synthesize these references.
|
||||
'emits USES edges for generic type arguments',
|
||||
]),
|
||||
go: new Set([
|
||||
// The legacy DAG path does not resolve method calls when the method is
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
CLASS_SCHEMA,
|
||||
INTERFACE_SCHEMA,
|
||||
METHOD_SCHEMA,
|
||||
PROPERTY_SCHEMA,
|
||||
CODE_ELEMENT_SCHEMA,
|
||||
COMMUNITY_SCHEMA,
|
||||
PROCESS_SCHEMA,
|
||||
|
|
@ -117,6 +118,11 @@ describe('LadybugDB Schema', () => {
|
|||
expect(FUNCTION_SCHEMA).toContain('isExported BOOLEAN');
|
||||
});
|
||||
|
||||
it('Property schema preserves declaredType', () => {
|
||||
expect(SCHEMA_QUERIES).toContain(PROPERTY_SCHEMA);
|
||||
expect(PROPERTY_SCHEMA).toContain('declaredType STRING');
|
||||
});
|
||||
|
||||
it('Community schema has heuristicLabel and cohesion', () => {
|
||||
expect(COMMUNITY_SCHEMA).toContain('heuristicLabel STRING');
|
||||
expect(COMMUNITY_SCHEMA).toContain('cohesion DOUBLE');
|
||||
|
|
|
|||
|
|
@ -422,4 +422,47 @@ describe('emitCsharpScopeCaptures — references', () => {
|
|||
expect(m!['@reference.receiver'].text).toBe('obj');
|
||||
expect(m!['@reference.name'].text).toBe('Name');
|
||||
});
|
||||
|
||||
it('captures member reads `obj.Name`', () => {
|
||||
const m = findMatch('class A { void M(User obj) { var name = obj.Name; } }', (t) =>
|
||||
t.includes('@reference.read.member'),
|
||||
);
|
||||
expect(m).toBeDefined();
|
||||
expect(m!['@reference.receiver'].text).toBe('obj');
|
||||
expect(m!['@reference.name'].text).toBe('Name');
|
||||
});
|
||||
|
||||
it('does not capture member calls as member reads', () => {
|
||||
const matches = emitCsharpScopeCaptures(
|
||||
'class A { void M(User obj) { obj.Save(); } }',
|
||||
'test.cs',
|
||||
);
|
||||
expect(matches.some((m) => '@reference.call.member' in m)).toBe(true);
|
||||
expect(matches.some((m) => '@reference.read.member' in m)).toBe(false);
|
||||
});
|
||||
|
||||
it('captures generic type arguments as type references', () => {
|
||||
const matches = emitCsharpScopeCaptures(
|
||||
'class A : IEntityTypeConfiguration<USER_INFO> { public Task<List<USER_INFO>> Load(List<USER_INFO> users) => null!; }',
|
||||
'test.cs',
|
||||
);
|
||||
const names = matches
|
||||
.filter((m) => '@reference.type' in m)
|
||||
.map((m) => m['@reference.name'].text);
|
||||
|
||||
expect(names).toContain('USER_INFO');
|
||||
expect(names).not.toContain('string');
|
||||
});
|
||||
|
||||
it('captures call-site generic type arguments as type references', () => {
|
||||
const matches = emitCsharpScopeCaptures(
|
||||
'class A { void M(IRepo repo) { repo.Get<USER_INFO>(); } }',
|
||||
'test.cs',
|
||||
);
|
||||
const names = matches
|
||||
.filter((m) => '@reference.type' in m)
|
||||
.map((m) => m['@reference.name'].text);
|
||||
|
||||
expect(names).toContain('USER_INFO');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue