mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
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:
parent
ae49c4cce7
commit
f847685bfe
15 changed files with 204 additions and 8 deletions
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
15
gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/App.cpp
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/App.cpp
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/Repo.h
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/Repo.h
vendored
Normal 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_;
|
||||
};
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/User.h
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-deref-range-for/User.h
vendored
Normal 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_;
|
||||
};
|
||||
17
gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/App.cs
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/App.cs
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/Repo.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
public class Repo {
|
||||
public string Name { get; set; }
|
||||
public void Save() {}
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/User.cs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/csharp-nested-member-foreach/User.cs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
public class User {
|
||||
public string Name { get; set; }
|
||||
public void Save() {}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue