fix(r): create an R6 field Property for every non-function value kind

The R6 public/private field queries listed only some expression kinds, so
`public = list(x = NA)` (also Inf, NaN, complex, if/for/while/repeat, a$b,
pkg::v, v[1], v[[1]], {..}, (..)) produced no Property although
RFieldExtractor accepts any value that is not a function_definition.

Reproduction: new test/unit/r-r6-field-values.test.ts failed 46 of 76 cases
on the unfixed query (expected ['x'], received []). The query now enumerates
every named grammar expression kind except function_definition, shared by the
namespaced and bare R6Class patterns.
This commit is contained in:
Mick Cooney 2026-10-15 20:31:40 +01:00
parent f114d5e754
commit 3b8682f23e
2 changed files with 98 additions and 2 deletions

View file

@ -3051,6 +3051,19 @@ export const ZIG_QUERIES = `
function: (field_expression
member: (identifier) @call.name)) @call
`;
/**
* Every named expression kind the R grammar allows as an argument value, except
* `function_definition` (an R6 entry holding a function is a method, not a field).
* Mirrors RFieldExtractor, which accepts any non-function value, so the query and
* the extractor agree on what a field is.
*/
const R6_FIELD_VALUE = `[
(binary_operator) (braced_expression) (break) (call) (complex) (dot_dot_i) (dots)
(extract_operator) (false) (float) (for_statement) (identifier) (if_statement) (inf)
(integer) (na) (namespace_operator) (nan) (next) (null) (parenthesized_expression)
(repeat_statement) (return) (string) (subset) (subset2) (true) (unary_operator)
(while_statement)
]`;
export const R_QUERIES = `
; ── Functions (name <- function(...) or name = function(...)) ────────────────
(binary_operator
@ -3199,7 +3212,7 @@ export const R_QUERIES = `
arguments: (arguments
(argument
name: (identifier) @name
value: [(string) (integer) (float) (true) (false) (null) (identifier) (call) (unary_operator) (binary_operator)] @_val) @definition.property))))))
value: ${R6_FIELD_VALUE} @_val) @definition.property))))))
; ── R6 Fields via bare call ──────────────────────────────────────────────────
(binary_operator
@ -3217,7 +3230,7 @@ export const R_QUERIES = `
arguments: (arguments
(argument
name: (identifier) @name
value: [(string) (integer) (float) (true) (false) (null) (identifier) (call) (unary_operator) (binary_operator)] @_val2) @definition.property))))))
value: ${R6_FIELD_VALUE} @_val2) @definition.property))))))
; ── S4 slots / representation ────────────────────────────────────────────────
(call

View file

@ -0,0 +1,83 @@
/**
* R6 `public =` / `private =` list() entries become Property definitions for every
* value that is not a function (functions are methods). The query used to enumerate
* only some of the grammar's expression kinds, so `list(x = NA)` or
* `list(x = if (ready) 1 else 2)` created no Property although RFieldExtractor
* accepts any non-function value. These tests run the query itself, for both the
* `R6::R6Class` and the bare `R6Class` forms.
*/
import { describe, expect, it } from 'vitest';
import Parser from 'tree-sitter';
import R from '@eagleoutice/tree-sitter-r';
import { R_QUERIES } from '../../src/core/ingestion/tree-sitter-queries.js';
const language = R as Parameters<Parser['setLanguage']>[0];
const parser = new Parser();
parser.setLanguage(language);
const query = new Parser.Query(language, R_QUERIES);
/** Names of every `definition.property` the query captures, in source order. */
function properties(source: string): string[] {
const tree = parser.parse(source);
const out: string[] = [];
for (const match of query.matches(tree.rootNode)) {
const def = match.captures.find((c) => c.name === 'definition.property');
const name = match.captures.find((c) => c.name === 'name');
if (def && name) out.push(name.node.text);
}
return out;
}
const FORMS: Record<string, (body: string) => string> = {
bare: (body) => `X <- R6Class("X", public = list(${body}))`,
namespaced: (body) => `X <- R6::R6Class("X", public = list(${body}))`,
private: (body) => `X <- R6Class("X", private = list(${body}))`,
};
// One value per grammar expression kind that can start an R expression, except
// function_definition (a method, not a field).
const VALUES: Record<string, string> = {
string: '"a"',
integer: '1L',
float: '1.5',
complex: '2i',
true: 'TRUE',
false: 'FALSE',
null: 'NULL',
na: 'NA',
na_character: 'NA_character_',
inf: 'Inf',
nan: 'NaN',
identifier: 'y',
call: 'g()',
unary_operator: '-1',
binary_operator: '1 + 2',
if_statement: 'if (ready) 1 else 2',
parenthesized_expression: '(1)',
extract_operator: 'a$b',
namespace_operator: 'pkg::value',
subset: 'v[1]',
subset2: 'v[[1]]',
braced_expression: '{ 1 }',
for_statement: 'for (i in 1:2) i',
while_statement: 'while (FALSE) 1',
repeat_statement: 'repeat break',
};
describe('R6 field values of every expression kind create a Property', () => {
for (const [formName, wrap] of Object.entries(FORMS)) {
for (const [kind, value] of Object.entries(VALUES)) {
it(`${formName}: ${kind} (${value})`, () => {
expect(properties(wrap(`x = ${value}`))).toEqual(['x']);
});
}
}
it('still treats function values as methods, not fields', () => {
expect(properties('X <- R6Class("X", public = list(m = function() 1, f = 2))')).toEqual(['f']);
expect(properties('X <- R6Class("X", public = list(m = \\() 1, f = 2))')).toEqual(['f']);
expect(properties('X <- R6::R6Class("X", public = list(m = function() 1, f = NA))')).toEqual([
'f',
]);
});
});