mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-30 06:42:43 +00:00
fix(r): read slots and fields given as c(...) vectors
The slot pattern matched only list(...) and representation(...), although
the field extractor accepts any call, so setClass("A", slots = c(a =
"numeric")) and setRefClass("R", fields = c(...)) created no Property nodes.
Accept c(...) for both. An unnamed string in slots = c("a", "b") or
fields = c("a", "b") names an untyped member, as in R; an unnamed string in
representation(...) or list(...) is still not a member. The new pattern
captures its callee as @_vecOwner, so the class-naming capture filter does
not mistake these strings for the class name.
This commit is contained in:
parent
0476330429
commit
e45fa2e080
7 changed files with 181 additions and 2 deletions
|
|
@ -18,6 +18,7 @@ Definitions:
|
|||
- S4 classes (`setClass`), generics (`setGeneric`), methods (`setMethod`) and slots (`representation`, `slots`).
|
||||
- R6 classes with their methods, fields and active bindings.
|
||||
- RefClass (`setRefClass`) classes with their methods and fields.
|
||||
- Slots and fields given as `list(...)`, `representation(...)` or `c(...)`. A named entry (`c(a = "numeric")`) records its type; an unnamed string in `slots = c("a", "b")` or `fields = c("a", "b")` names an untyped member, as in R. An unnamed string in `representation(...)` or `list(...)` is not a member.
|
||||
- The argument that names an S4 or RefClass class, a generic or a method is the argument spelled with the first formal (`Class=`, `name=`, `f=`) wherever it sits, else the first unnamed argument (so a later `Class=` takes the formal from an earlier unnamed argument, as in R). Other strings (`contains=`, `valueClass=`, a `setMethod` signature) create no node.
|
||||
|
||||
Types:
|
||||
|
|
|
|||
|
|
@ -129,13 +129,32 @@ export class RFieldExtractor extends BaseFieldExtractor {
|
|||
const slotArgs = slotCall.childForFieldName('arguments');
|
||||
if (!slotArgs) continue;
|
||||
|
||||
// Only an unnamed character vector (`slots = c("a", "b")`) names its slots by
|
||||
// their strings; elsewhere an unnamed string is not a slot (`representation("Base")`).
|
||||
const unnamedNamesSlots =
|
||||
paramName !== 'representation' && slotCall.childForFieldName('function')?.text === 'c';
|
||||
|
||||
for (let j = 0; j < slotArgs.namedChildCount; j++) {
|
||||
const slot = slotArgs.namedChild(j);
|
||||
if (!slot || slot.type !== 'argument') continue;
|
||||
|
||||
const slotName = slot.childForFieldName('name');
|
||||
const slotType = slot.childForFieldName('value');
|
||||
if (!slotName) continue;
|
||||
if (!slotName) {
|
||||
const unnamed = unnamedNamesSlots ? extractQuotedTypeName(slotType) : null;
|
||||
if (unnamed) {
|
||||
fields.push({
|
||||
name: unnamed,
|
||||
type: null,
|
||||
visibility: 'public',
|
||||
isStatic: false,
|
||||
isReadonly: false,
|
||||
sourceFile: context.filePath,
|
||||
line: slot.startPosition.row + 1,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const typeName = extractQuotedTypeName(slotType);
|
||||
|
||||
|
|
|
|||
|
|
@ -3242,10 +3242,31 @@ export const R_QUERIES = `
|
|||
(#match? @_slotArg "^(representation|slots|fields)$")
|
||||
value: (call
|
||||
function: (identifier) @_listfn
|
||||
(#match? @_listfn "^(list|representation)$")
|
||||
(#match? @_listfn "^(list|representation|c)$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @name) @definition.property)))))
|
||||
|
||||
; ── S4 / RefClass unnamed names: slots = c("a", "b"), fields = c("a", "b") ──
|
||||
; An unnamed character vector names the slots or fields (their class is ANY). Only
|
||||
; c(...) reads this way: an unnamed string in representation(...) is a superclass.
|
||||
; The callee is captured as @_vecOwner, not @_fn, so these string captures are not
|
||||
; mistaken for the class-naming argument by isRNonNamingArgumentCapture.
|
||||
(call
|
||||
function: (identifier) @_vecOwner
|
||||
(#match? @_vecOwner "^(setClass|setRefClass)$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @_vecArg
|
||||
(#match? @_vecArg "^(slots|fields)$")
|
||||
value: (call
|
||||
function: (identifier) @_vecfn
|
||||
(#match? @_vecfn "^c$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
!name
|
||||
value: (string
|
||||
content: (string_content) @name)) @definition.property)))))
|
||||
`;
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
|
|
|||
5
gitnexus/test/fixtures/lang-resolution/r-vector-members/R/vectors.R
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/r-vector-members/R/vectors.R
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# slots / fields given as c(...) rather than list(...).
|
||||
setClass("Person", slots = c(name = "character", age = "numeric"))
|
||||
setClass("Tagged", slots = c("tag", "weight"))
|
||||
Account <- setRefClass("Account", fields = c(balance = "numeric"))
|
||||
Ledger <- setRefClass("Ledger", fields = c("entries"))
|
||||
45
gitnexus/test/integration/resolvers/r-vector-members.test.ts
Normal file
45
gitnexus/test/integration/resolvers/r-vector-members.test.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* R: `slots = c(...)` and `fields = c(...)` are the forms `methods::setClass` and
|
||||
* `setRefClass` document. They used to create no Property nodes, although the
|
||||
* `slots = list(...)` and `representation(...)` forms did.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import path from 'path';
|
||||
import { FIXTURES, getRelationships, runPipelineFromRepo } from './helpers.js';
|
||||
import type { PipelineResult } from './helpers.js';
|
||||
|
||||
// Cold worker-pool startups otherwise flake against the 5s default ready budget
|
||||
// on a loaded runner, failing for reasons unrelated to R parsing or resolution.
|
||||
// Safe under `pool: 'forks'`: the env stub is process-global, but each file gets its own process.
|
||||
beforeAll(() => vi.stubEnv('GITNEXUS_WORKER_READY_TIMEOUT_MS', '60000'));
|
||||
afterAll(() => vi.unstubAllEnvs());
|
||||
|
||||
describe('R slots / fields given as a c(...) vector', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'r-vector-members'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('creates a Property for every named and unnamed entry, owned by its class', () => {
|
||||
const owned = getRelationships(result, 'HAS_PROPERTY')
|
||||
.map((e) => `${e.source}->${e.target}`)
|
||||
.sort();
|
||||
expect(owned).toEqual([
|
||||
'Account->balance',
|
||||
'Ledger->entries',
|
||||
'Person->age',
|
||||
'Person->name',
|
||||
'Tagged->tag',
|
||||
'Tagged->weight',
|
||||
]);
|
||||
});
|
||||
|
||||
it('records the declared type of a named entry and none for an unnamed one', () => {
|
||||
const declared = (name: string) =>
|
||||
result.graph.getNode(`Property:R/vectors.R:${name}`)?.properties.declaredType;
|
||||
expect(declared('Person.name')).toBe('character');
|
||||
expect(declared('Person.age')).toBe('numeric');
|
||||
expect(declared('Tagged.tag')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -117,6 +117,57 @@ describe('R_QUERIES: setClass / setRefClass name only their first argument', ()
|
|||
});
|
||||
});
|
||||
|
||||
describe('R_QUERIES: slots = c(...) and fields = c(...) define properties', () => {
|
||||
it('reads a named vector: c(a = "numeric")', () => {
|
||||
expect(definitions('setClass("A", slots = c(a = "numeric", b = "character"))')).toEqual([
|
||||
'definition.class:A',
|
||||
'definition.property:a',
|
||||
'definition.property:b',
|
||||
]);
|
||||
expect(definitions('setRefClass("R", fields = c(n = "numeric"))')).toEqual([
|
||||
'definition.class:R',
|
||||
'definition.property:n',
|
||||
]);
|
||||
});
|
||||
|
||||
it('reads an unnamed character vector as names: c("a", "b")', () => {
|
||||
expect(definitions('setClass("A", slots = c("a", "b"))')).toEqual([
|
||||
'definition.class:A',
|
||||
'definition.property:a',
|
||||
'definition.property:b',
|
||||
]);
|
||||
expect(definitions('setRefClass("R", fields = c("n", "m"))')).toEqual([
|
||||
'definition.class:R',
|
||||
'definition.property:n',
|
||||
'definition.property:m',
|
||||
]);
|
||||
});
|
||||
|
||||
it('reads a vector that mixes named and unnamed entries', () => {
|
||||
expect(definitions('setClass("A", slots = c("a", b = "numeric"))')).toEqual([
|
||||
'definition.class:A',
|
||||
'definition.property:a',
|
||||
'definition.property:b',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not read a string inside a vector under another argument name', () => {
|
||||
expect(definitions('setClass("A", contains = c("B", "C"))')).toEqual(['definition.class:A']);
|
||||
expect(definitions('setClass("A", prototype = c(x = "1"))')).toEqual(['definition.class:A']);
|
||||
});
|
||||
|
||||
it('does not read unnamed strings of representation() or list() as slots', () => {
|
||||
expect(definitions('setClass("A", representation("VIRTUAL"))')).toEqual(['definition.class:A']);
|
||||
expect(definitions('setClass("A", slots = list("a"))')).toEqual(['definition.class:A']);
|
||||
});
|
||||
|
||||
it('does not read other vector builders', () => {
|
||||
expect(definitions('setClass("A", slots = character(a = "numeric"))')).toEqual([
|
||||
'definition.class:A',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('R_QUERIES: setGeneric names only its first argument', () => {
|
||||
it('names the generic from the first positional string (positive control)', () => {
|
||||
expect(definitions('setGeneric("f", function(x) standardGeneric("f"))')).toEqual([
|
||||
|
|
|
|||
|
|
@ -120,3 +120,40 @@ describe('R field extractor: slots attach to the Class argument', () => {
|
|||
expect(propertyOwner(source)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RFieldExtractor: slots = c(...) / fields = c(...)', () => {
|
||||
const details = (source: string) =>
|
||||
extractor
|
||||
.extract(classCall(source), context)
|
||||
?.fields.map((f) => [f.name, f.type] as [string, string | null]);
|
||||
|
||||
it('reads the names and types of a named vector', () => {
|
||||
expect(details('setClass("A", slots = c(a = "numeric", b = "character"))')).toEqual([
|
||||
['a', 'numeric'],
|
||||
['b', 'character'],
|
||||
]);
|
||||
expect(details('setRefClass("R", fields = c(n = "numeric"))')).toEqual([['n', 'numeric']]);
|
||||
});
|
||||
|
||||
it('reads the strings of an unnamed vector as untyped names', () => {
|
||||
expect(details('setClass("A", slots = c("a", "b"))')).toEqual([
|
||||
['a', null],
|
||||
['b', null],
|
||||
]);
|
||||
expect(details('setRefClass("R", fields = c("n"))')).toEqual([['n', null]]);
|
||||
});
|
||||
|
||||
it('keeps named and unnamed entries of one vector together', () => {
|
||||
expect(details('setClass("A", slots = c("a", b = "numeric"))')).toEqual([
|
||||
['a', null],
|
||||
['b', 'numeric'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not read an unnamed string of representation() or list()', () => {
|
||||
expect(
|
||||
details('setClass("A", representation = representation("VIRTUAL", x = "numeric"))'),
|
||||
).toEqual([['x', 'numeric']]);
|
||||
expect(details('setClass("A", slots = list("a"))')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue