diff --git a/gitnexus/src/core/ingestion/type-env.ts b/gitnexus/src/core/ingestion/type-env.ts index d6c575ae8..73682c3bd 100644 --- a/gitnexus/src/core/ingestion/type-env.ts +++ b/gitnexus/src/core/ingestion/type-env.ts @@ -414,7 +414,21 @@ export const buildTypeEnv = ( // (User[], []User, List[User]) that fail extractSimpleTypeName still get // their AST type node recorded for Strategy 1 for-loop resolution. // Try direct extraction first (works for Go var_spec, Python assignment, Rust let_declaration). - const typeNode = node.childForFieldName('type'); + // Try direct type field first, then unwrap wrapper nodes (C# field_declaration, + // local_declaration_statement wrap their type inside a variable_declaration child). + let typeNode = node.childForFieldName('type'); + if (!typeNode) { + // C# field_declaration / local_declaration_statement wrap type inside variable_declaration. + // Use manual loop instead of namedChildren.find() to avoid array allocation on hot path. + let wrapped = node.childForFieldName('declaration'); + if (!wrapped) { + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c?.type === 'variable_declaration') { wrapped = c; break; } + } + } + if (wrapped) typeNode = wrapped.childForFieldName('type'); + } if (typeNode) { const nameNode = node.childForFieldName('name') ?? node.childForFieldName('left') diff --git a/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts b/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts index e02ba3f50..0e92902a1 100644 --- a/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts +++ b/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts @@ -334,6 +334,11 @@ const extractForLoopBinding: ForLoopExtractor = ( const field = fieldExpr.lastNamedChild; if (field?.type === 'field_identifier') methodName = field.text; } + } else if (rightNode.type === 'pointer_expression') { + // Dereference: for (auto& user : *ptr) → pointer_expression > identifier + // Only handles simple *identifier; *this->field and **ptr are not resolved. + const operand = rightNode.lastNamedChild; + if (operand?.type === 'identifier') iterableName = operand.text; } if (!iterableName) return; diff --git a/gitnexus/src/core/ingestion/type-extractors/csharp.ts b/gitnexus/src/core/ingestion/type-extractors/csharp.ts index 9b9e87a84..e8b9cc3ab 100644 --- a/gitnexus/src/core/ingestion/type-extractors/csharp.ts +++ b/gitnexus/src/core/ingestion/type-extractors/csharp.ts @@ -224,7 +224,13 @@ const extractForLoopBinding: ForLoopExtractor = ( const prop = rightNode.childForFieldName('name'); const propText = prop?.type === 'identifier' ? prop.text : undefined; if (propText && KNOWN_CONTAINER_PROPS.has(propText)) { - if (obj?.type === 'identifier') iterableName = obj.text; + if (obj?.type === 'identifier') { + iterableName = obj.text; + } else if (obj?.type === 'member_access_expression') { + // Nested member access: this.data.Values → obj is "this.data", extract "data" + const innerProp = obj.childForFieldName('name'); + if (innerProp) iterableName = innerProp.text; + } methodName = propText; } else if (propText) { // Bare member access: this.users → use property name for scopeEnv lookup diff --git a/gitnexus/src/core/ingestion/type-extractors/jvm.ts b/gitnexus/src/core/ingestion/type-extractors/jvm.ts index c12d85703..4c4a4bdcc 100644 --- a/gitnexus/src/core/ingestion/type-extractors/jvm.ts +++ b/gitnexus/src/core/ingestion/type-extractors/jvm.ts @@ -160,9 +160,15 @@ const extractJavaForLoopBinding: ForLoopExtractor = ( if (field) iterableName = field.text; } else if (iterableNode.type === 'method_invocation') { // data.keySet() → method_invocation > object: identifier + name: identifier + // Also handles this.data.values() → object is field_access, extract inner field name const obj = iterableNode.childForFieldName('object'); const name = iterableNode.childForFieldName('name'); - if (obj?.type === 'identifier') iterableName = obj.text; + if (obj?.type === 'identifier') { + iterableName = obj.text; + } else if (obj?.type === 'field_access') { + const innerField = obj.childForFieldName('field'); + if (innerField) iterableName = innerField.text; + } if (name) methodName = name.text; } if (!iterableName) return; diff --git a/gitnexus/src/core/ingestion/type-extractors/shared.ts b/gitnexus/src/core/ingestion/type-extractors/shared.ts index e79b10b83..4756eb156 100644 --- a/gitnexus/src/core/ingestion/type-extractors/shared.ts +++ b/gitnexus/src/core/ingestion/type-extractors/shared.ts @@ -207,9 +207,16 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode, depth = 0): string | } } + // C++ template_type (e.g., vector, map): extract base name + if (typeNode.type === 'template_type') { + const base = typeNode.childForFieldName('name') ?? typeNode.firstNamedChild; + if (base) return extractSimpleTypeName(base, depth + 1); + } + // Generic types: extract the base type (e.g., List → List) // For nullable wrappers (Optional, Option), unwrap to inner type. - if (typeNode.type === 'generic_type' || typeNode.type === 'parameterized_type') { + if (typeNode.type === 'generic_type' || typeNode.type === 'parameterized_type' + || typeNode.type === 'generic_name') { const base = typeNode.childForFieldName('name') ?? typeNode.childForFieldName('type') ?? typeNode.firstNamedChild; @@ -345,8 +352,9 @@ export const extractGenericTypeArgs = (typeNode: SyntaxNode, depth = 0): string[ return []; } - // Only process generic/parameterized type nodes - if (typeNode.type !== 'generic_type' && typeNode.type !== 'parameterized_type') { + // Only process generic/parameterized type nodes (includes C#'s generic_name) + if (typeNode.type !== 'generic_type' && typeNode.type !== 'parameterized_type' + && typeNode.type !== 'generic_name') { return []; } diff --git a/gitnexus/src/core/ingestion/type-extractors/typescript.ts b/gitnexus/src/core/ingestion/type-extractors/typescript.ts index f7ebde477..a6655a53d 100644 --- a/gitnexus/src/core/ingestion/type-extractors/typescript.ts +++ b/gitnexus/src/core/ingestion/type-extractors/typescript.ts @@ -81,8 +81,7 @@ const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map + +void processUsers(std::vector* usersPtr) { + for (auto& user : *usersPtr) { + user.save(); + } +} + +void processRepos(std::vector* reposPtr) { + for (const auto& repo : *reposPtr) { + repo.save(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/Repo.h b/gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/Repo.h new file mode 100644 index 000000000..b3858517c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/Repo.h @@ -0,0 +1,10 @@ +#pragma once +#include + +class Repo { +public: + Repo(const std::string& name) : name_(name) {} + void save() {} +private: + std::string name_; +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/User.h b/gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/User.h new file mode 100644 index 000000000..fdb227354 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/User.h @@ -0,0 +1,10 @@ +#pragma once +#include + +class User { +public: + User(const std::string& name) : name_(name) {} + void save() {} +private: + std::string name_; +}; diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/App.cs b/gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/App.cs new file mode 100644 index 000000000..3550d259a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/App.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +public class App { + private Dictionary data; + + public void ProcessValues() { + foreach (var user in this.data.Values) { + user.Save(); + } + } + + public void ProcessKeys() { + foreach (var key in this.data.Keys) { + key.ToString(); + } + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/Repo.cs b/gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/Repo.cs new file mode 100644 index 000000000..0e8416a76 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/Repo.cs @@ -0,0 +1,4 @@ +public class Repo { + public string Name { get; set; } + public void Save() {} +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/User.cs b/gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/User.cs new file mode 100644 index 000000000..57c04e93b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/User.cs @@ -0,0 +1,4 @@ +public class User { + public string Name { get; set; } + public void Save() {} +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 063faf574..23aca2322 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -768,3 +768,47 @@ describe('C++ structured binding in range-for', () => { expect(wrongSave).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// C++ pointer dereference in range-for: for (auto& user : *ptr) +// --------------------------------------------------------------------------- + +describe('C++ pointer dereference in range-for', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-deref-range-for'), + () => {}, + ); + }, 60000); + + it('detects User and Repo classes with save methods', () => { + expect(getNodesByLabel(result, 'Class')).toContain('User'); + expect(getNodesByLabel(result, 'Class')).toContain('Repo'); + }); + + it('resolves user.save() in *usersPtr range-for to User#save', () => { + const calls = getRelationships(result, 'CALLS'); + const userSave = calls.find(c => + c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('User'), + ); + expect(userSave).toBeDefined(); + }); + + it('resolves repo.save() in *reposPtr range-for to Repo#save', () => { + const calls = getRelationships(result, 'CALLS'); + const repoSave = calls.find(c => + c.target === 'save' && c.source === 'processRepos' && c.targetFilePath?.includes('Repo'), + ); + expect(repoSave).toBeDefined(); + }); + + it('does NOT cross-resolve user.save() to Repo#save (negative)', () => { + const calls = getRelationships(result, 'CALLS'); + const wrongSave = calls.find(c => + c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('Repo'), + ); + expect(wrongSave).toBeUndefined(); + }); +}); diff --git a/gitnexus/test/integration/resolvers/csharp.test.ts b/gitnexus/test/integration/resolvers/csharp.test.ts index 59a93ca99..2275e908c 100644 --- a/gitnexus/test/integration/resolvers/csharp.test.ts +++ b/gitnexus/test/integration/resolvers/csharp.test.ts @@ -1158,3 +1158,38 @@ describe('C# recursive_pattern type resolution', () => { expect(toRepo.length).toBe(1); }); }); + +// --------------------------------------------------------------------------- +// C# nested member access with container property: this.data.Values +// --------------------------------------------------------------------------- + +describe('C# nested member access foreach (this.data.Values)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'csharp-nested-member-foreach'), + () => {}, + ); + }, 60000); + + it('detects User class with Save method', () => { + expect(getNodesByLabel(result, 'Class')).toContain('User'); + }); + + it('resolves user.Save() via this.data.Values to User#Save', () => { + const calls = getRelationships(result, 'CALLS'); + const userSave = calls.find(c => + c.target === 'Save' && c.source === 'ProcessValues' && c.targetFilePath?.includes('User'), + ); + expect(userSave).toBeDefined(); + }); + + it('does NOT resolve user.Save() to Repo#Save (negative)', () => { + const calls = getRelationships(result, 'CALLS'); + const wrongSave = calls.find(c => + c.target === 'Save' && c.source === 'ProcessValues' && c.targetFilePath?.includes('Repo'), + ); + expect(wrongSave).toBeUndefined(); + }); +}); diff --git a/gitnexus/test/unit/type-env.test.ts b/gitnexus/test/unit/type-env.test.ts index 030534057..b4102de22 100644 --- a/gitnexus/test/unit/type-env.test.ts +++ b/gitnexus/test/unit/type-env.test.ts @@ -3458,6 +3458,25 @@ public class App { const { env } = buildTypeEnv(tree, 'csharp'); expect(flatGet(env, 'user')).toBe('User'); }); + + it('foreach (var user in this.data.Values) — nested member access with container property', () => { + const tree = parse(` +using System.Collections.Generic; +public class App { + private Dictionary data; + public void ProcessValues() { + foreach (var user in this.data.Values) { + user.Save(); + } + } +} + `, CSharp); + const { env, lookup } = buildTypeEnv(tree, 'csharp'); + expect(flatGet(env, 'user')).toBe('User'); + // Verify lookup works from the call site (user.Save()) + const saveCall = tree.rootNode.descendantsOfType('invocation_expression')[0]; + expect(lookup('user', saveCall)).toBe('User'); + }); }); describe('TypeScript class field declaration (Phase 6.1)', () => {