feat: Phase 6.2 review findings — C# nested member foreach, C++ deref range-for, Java field_access

Close two gaps found during fourth-pass review of PR #318:

- C# foreach (var user in this.data.Values): nested member_access_expression
  now extracts intermediate property name for scopeEnv lookup
- C++ for (auto& user : *ptr): pointer_expression dereference now recognized
  as range-for iterable

Root causes fixed in shared infrastructure:
- extractSimpleTypeName: add template_type (C++) and generic_name (C#)
- extractGenericTypeArgs: add generic_name for consistency
- type-env.ts: unwrap variable_declaration wrapper in field_declaration
  for declarationTypeNodes capture (zero-allocation manual loop)

Additional review findings addressed:
- Java: add field_access handler for this.data.values() in method_invocation
- C++ pointer_expression: document limitation (*identifier only)
- TypeScript: fix stale comment about property_identifier

All 525 tests pass (278 unit + 247 integration).
This commit is contained in:
Gergo Magyar 2026-03-17 16:21:47 +00:00
parent ae49c4cce7
commit f847685bfe
15 changed files with 204 additions and 8 deletions

View file

@ -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')

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -207,9 +207,16 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode, depth = 0): string |
}
}
// C++ template_type (e.g., vector<User>, map<string, User>): 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<User> → List)
// For nullable wrappers (Optional<User>, Option<User>), 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 [];
}

View file

@ -81,8 +81,7 @@ const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<str
return;
}
// Class field: `private users: User[]` — public_field_definition has name + type fields directly
// The name child is a property_identifier (not handled by extractVarName), so we read .text directly.
// Class field: `private users: User[]` — public_field_definition has name + type fields directly.
if (node.type === 'public_field_definition') {
const nameNode = node.childForFieldName('name');
const typeAnnotation = node.childForFieldName('type');

View file

@ -0,0 +1,15 @@
#include "User.h"
#include "Repo.h"
#include <vector>
void processUsers(std::vector<User>* usersPtr) {
for (auto& user : *usersPtr) {
user.save();
}
}
void processRepos(std::vector<Repo>* reposPtr) {
for (const auto& repo : *reposPtr) {
repo.save();
}
}

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
class Repo {
public:
Repo(const std::string& name) : name_(name) {}
void save() {}
private:
std::string name_;
};

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
class User {
public:
User(const std::string& name) : name_(name) {}
void save() {}
private:
std::string name_;
};

View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
public class App {
private Dictionary<string, User> 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();
}
}
}

View file

@ -0,0 +1,4 @@
public class Repo {
public string Name { get; set; }
public void Save() {}
}

View file

@ -0,0 +1,4 @@
public class User {
public string Name { get; set; }
public void Save() {}
}

View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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<string, User> 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)', () => {