mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-15 04:18:05 +00:00
refactor(ingestion): add LanguageProvider.resolveMemberOwnerNode and move R's owner-node lookup behind it
Add an optional resolveMemberOwnerNode hook consulted by the worker's enclosing-owner helper after the container walk and resolveFileTypeOwner. R implements it via rResolveMemberOwnerNode, replacing the two language === R branches in parse-worker.
This commit is contained in:
parent
1e7985db60
commit
c2fcdd1bc5
5 changed files with 135 additions and 14 deletions
|
|
@ -385,6 +385,16 @@ interface LanguageProviderConfig {
|
|||
filePath: string,
|
||||
) => { readonly name: string; readonly label: NodeLabel } | null;
|
||||
|
||||
/**
|
||||
* For languages whose member owner is not a syntactic ancestor container
|
||||
* (R: `R6Class(...)` / `setRefClass(...)` calls): return the node the
|
||||
* method/field extractors read members from, or `null`.
|
||||
*
|
||||
* Consulted by the enclosing-owner slot after the container walk and
|
||||
* `resolveFileTypeOwner` (same slot as the Zig file-owner).
|
||||
* Default: undefined. */
|
||||
readonly resolveMemberOwnerNode?: (definitionNode: SyntaxNode) => SyntaxNode | null;
|
||||
|
||||
/**
|
||||
* The type a CONTAINER node declares, when the language names it from its
|
||||
* context rather than from a name child of the node — a binding wrapper,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { rMethodExtractor } from '../method-extractors/r.js';
|
|||
import { emitRScopeCaptures } from './r/captures.js';
|
||||
import { interpretRImport, interpretRTypeBinding } from './r/interpret.js';
|
||||
import { rPostParse } from './r/post-parse.js';
|
||||
import { rResolveMemberOwnerNode } from './r/owner-hooks.js';
|
||||
|
||||
const R_BUILT_INS: ReadonlySet<string> = new Set([
|
||||
// Base R
|
||||
|
|
@ -185,4 +186,5 @@ export const rProvider = defineLanguage({
|
|||
interpretImport: interpretRImport,
|
||||
interpretTypeBinding: interpretRTypeBinding,
|
||||
postParse: rPostParse,
|
||||
resolveMemberOwnerNode: rResolveMemberOwnerNode,
|
||||
});
|
||||
|
|
|
|||
15
gitnexus/src/core/ingestion/languages/r/owner-hooks.ts
Normal file
15
gitnexus/src/core/ingestion/languages/r/owner-hooks.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* R owner hooks for the shared ingestion pipeline.
|
||||
*
|
||||
* R has no class syntax: R6 / RefClass members belong to a call or assignment
|
||||
* (`Foo <- R6::R6Class(...)`, `setRefClass(...)`), not to an enclosing
|
||||
* container node. `resolveMemberOwnerNode` hands the member extractors that
|
||||
* owner node.
|
||||
*/
|
||||
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { findRFieldOwnerNode } from '../../field-extractors/r.js';
|
||||
|
||||
/** `LanguageProvider.resolveMemberOwnerNode` for R. */
|
||||
export const rResolveMemberOwnerNode = (definitionNode: SyntaxNode): SyntaxNode | null =>
|
||||
findRFieldOwnerNode(definitionNode);
|
||||
|
|
@ -179,7 +179,7 @@ import {
|
|||
DEFAULT_PDG_MAX_FUNCTION_LINES,
|
||||
type CfgSkipCounts,
|
||||
} from '../cfg/collect.js';
|
||||
import { findRFieldOwnerNode, getRTopLevelPropertyOwnerName } from '../field-extractors/r.js';
|
||||
import { getRTopLevelPropertyOwnerName } from '../field-extractors/r.js';
|
||||
import { getRTopLevelMethodOwnerName } from '../method-extractors/r.js';
|
||||
|
||||
import { logger } from '../../logger.js';
|
||||
|
|
@ -685,7 +685,9 @@ function findEnclosingClassNode(node: SyntaxNode): SyntaxNode | null {
|
|||
* a type (`resolveFileTypeOwner`, e.g. a Zig file-struct), the tree root is
|
||||
* the owner node the method/field extractors should read members from. Same
|
||||
* root, same name as `findEnclosingClassInfo`'s root branch, so member ids and
|
||||
* owner ids agree.
|
||||
* owner ids agree. Failing both, the provider's `resolveMemberOwnerNode` gets
|
||||
* the last word (languages whose members belong to a call/assignment rather
|
||||
* than an enclosing container node, e.g. R's `R6Class(...)`).
|
||||
*/
|
||||
function findEnclosingClassNodeOrFileOwner(
|
||||
node: SyntaxNode,
|
||||
|
|
@ -694,10 +696,12 @@ function findEnclosingClassNodeOrFileOwner(
|
|||
): SyntaxNode | null {
|
||||
const container = findEnclosingClassNode(node);
|
||||
if (container !== null) return container;
|
||||
if (provider.resolveFileTypeOwner === undefined) return null;
|
||||
let root: SyntaxNode = node;
|
||||
while (root.parent) root = root.parent;
|
||||
return provider.resolveFileTypeOwner(root, filePath) !== null ? root : null;
|
||||
if (provider.resolveFileTypeOwner !== undefined) {
|
||||
let root: SyntaxNode = node;
|
||||
while (root.parent) root = root.parent;
|
||||
if (provider.resolveFileTypeOwner(root, filePath) !== null) return root;
|
||||
}
|
||||
return provider.resolveMemberOwnerNode?.(node) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2816,13 +2820,9 @@ const processFileGroup = (
|
|||
// returnType, isAbstract/isFinal/annotations, visibility, and more.
|
||||
let enrichedByMethodExtractor = false;
|
||||
if (provider.methodExtractor && definitionNode) {
|
||||
// R6/R5 classes are defined via function calls (R6::R6Class(), setRefClass()),
|
||||
// not tree-sitter class syntax, so findEnclosingClassNode can't find them.
|
||||
// findRFieldOwnerNode walks up looking for those function-call patterns.
|
||||
const classNode =
|
||||
findEnclosingClassNodeOrFileOwner(definitionNode, provider, file.path) ??
|
||||
findClassNodeByQualifiedName(definitionNode) ??
|
||||
(language === SupportedLanguages.R ? findRFieldOwnerNode(definitionNode) : null);
|
||||
findClassNodeByQualifiedName(definitionNode);
|
||||
if (classNode) {
|
||||
const methodMap = getMethodInfo(classNode, provider, {
|
||||
filePath: file.path,
|
||||
|
|
@ -3021,9 +3021,7 @@ const processFileGroup = (
|
|||
if (nodeLabel === 'Property' && definitionNode) {
|
||||
// FieldExtractor is the single source of truth when available
|
||||
if (provider.fieldExtractor && typeEnv) {
|
||||
const classNode =
|
||||
findEnclosingClassNodeOrFileOwner(definitionNode, provider, file.path) ??
|
||||
(language === SupportedLanguages.R ? findRFieldOwnerNode(definitionNode) : null);
|
||||
const classNode = findEnclosingClassNodeOrFileOwner(definitionNode, provider, file.path);
|
||||
if (classNode) {
|
||||
const fieldMap = getFieldInfo(classNode, provider, {
|
||||
typeEnv,
|
||||
|
|
|
|||
96
gitnexus/test/unit/r-owner-hooks.test.ts
Normal file
96
gitnexus/test/unit/r-owner-hooks.test.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { createParserForLanguage } from '../../src/core/tree-sitter/parser-loader.js';
|
||||
import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js';
|
||||
import { rResolveMemberOwnerNode } from '../../src/core/ingestion/languages/r/owner-hooks.js';
|
||||
import { rProvider } from '../../src/core/ingestion/languages/r.js';
|
||||
import { getProvider } from '../../src/core/ingestion/languages/index.js';
|
||||
|
||||
// Fork #19, step 5: R's member-owner node is found through
|
||||
// `LanguageProvider.resolveMemberOwnerNode`, not a `language === R` branch in the worker.
|
||||
// R classes are calls/assignments, not syntactic containers, so the hook returns the R6
|
||||
// `binary_operator` or the `setRefClass(...)` call that owns a member.
|
||||
|
||||
const SOURCE = `Foo <- R6::R6Class("Foo", public = list(n = 1, bar = function() 1))
|
||||
Acc <- setRefClass("Acc", fields = list(a = "numeric"), methods = list(m = function() 1))
|
||||
setMethod("area", "Circle", function(shape) 1)
|
||||
plain <- function(x) x
|
||||
`;
|
||||
|
||||
let root: SyntaxNode;
|
||||
|
||||
const walk = (n: SyntaxNode, visit: (n: SyntaxNode) => void): void => {
|
||||
visit(n);
|
||||
for (let i = 0; i < n.namedChildCount; i++) {
|
||||
const child = n.namedChild(i);
|
||||
if (child) walk(child, visit);
|
||||
}
|
||||
};
|
||||
|
||||
const find = (pred: (n: SyntaxNode) => boolean): SyntaxNode => {
|
||||
let hit: SyntaxNode | undefined;
|
||||
walk(root, (n) => {
|
||||
if (hit === undefined && pred(n)) hit = n;
|
||||
});
|
||||
if (hit === undefined) throw new Error('fixture node not found');
|
||||
return hit;
|
||||
};
|
||||
|
||||
const fnDefinitionOn = (row: number): SyntaxNode =>
|
||||
find((n) => n.type === 'function_definition' && n.startPosition.row === row);
|
||||
|
||||
beforeAll(async () => {
|
||||
const parser = await createParserForLanguage(SupportedLanguages.R);
|
||||
root = parser.parse(SOURCE).rootNode as unknown as SyntaxNode;
|
||||
});
|
||||
|
||||
describe('rResolveMemberOwnerNode', () => {
|
||||
it('resolves an R6 method to the enclosing `Foo <- R6Class(...)` binary_operator', () => {
|
||||
const owner = rResolveMemberOwnerNode(fnDefinitionOn(0));
|
||||
expect(owner?.type).toBe('binary_operator');
|
||||
expect(owner?.childForFieldName('lhs')?.text).toBe('Foo');
|
||||
});
|
||||
|
||||
it('resolves an R6 field to the same binary_operator', () => {
|
||||
const field = find((n) => n.type === 'argument' && n.childForFieldName('name')?.text === 'n');
|
||||
const owner = rResolveMemberOwnerNode(field);
|
||||
expect(owner?.type).toBe('binary_operator');
|
||||
expect(owner?.childForFieldName('lhs')?.text).toBe('Foo');
|
||||
});
|
||||
|
||||
it('resolves a RefClass method to the `setRefClass(...)` call', () => {
|
||||
const owner = rResolveMemberOwnerNode(fnDefinitionOn(1));
|
||||
expect(owner?.type).toBe('call');
|
||||
expect(owner?.childForFieldName('function')?.text).toBe('setRefClass');
|
||||
});
|
||||
|
||||
it('returns null for a top-level function', () => {
|
||||
expect(rResolveMemberOwnerNode(fnDefinitionOn(3))).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for a setMethod(...) call (its owner arrives via ownerNameHint, not a node)', () => {
|
||||
const call = find(
|
||||
(n) => n.type === 'call' && n.childForFieldName('function')?.text === 'setMethod',
|
||||
);
|
||||
expect(rResolveMemberOwnerNode(call)).toBeNull();
|
||||
expect(rResolveMemberOwnerNode(fnDefinitionOn(2))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveMemberOwnerNode provider wiring', () => {
|
||||
it('is registered on the R provider', () => {
|
||||
expect(rProvider.resolveMemberOwnerNode).toBe(rResolveMemberOwnerNode);
|
||||
expect(getProvider(SupportedLanguages.R).resolveMemberOwnerNode).toBe(rResolveMemberOwnerNode);
|
||||
});
|
||||
|
||||
it('is left undefined on other providers', () => {
|
||||
for (const lang of [
|
||||
SupportedLanguages.Python,
|
||||
SupportedLanguages.TypeScript,
|
||||
SupportedLanguages.Java,
|
||||
SupportedLanguages.Zig,
|
||||
]) {
|
||||
expect(getProvider(lang).resolveMemberOwnerNode).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue