From 3b8682f23ec5abd69d8a1f0eb5589da97c1b0cd8 Mon Sep 17 00:00:00 2001 From: Mick Cooney Date: Thu, 15 Oct 2026 20:31:40 +0100 Subject: [PATCH] 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. --- .../src/core/ingestion/tree-sitter-queries.ts | 17 +++- gitnexus/test/unit/r-r6-field-values.test.ts | 83 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 gitnexus/test/unit/r-r6-field-values.test.ts diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index c5b2c6134..4dff706f6 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -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 diff --git a/gitnexus/test/unit/r-r6-field-values.test.ts b/gitnexus/test/unit/r-r6-field-values.test.ts new file mode 100644 index 000000000..14eff71c0 --- /dev/null +++ b/gitnexus/test/unit/r-r6-field-values.test.ts @@ -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[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> = { + 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: '"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', + ]); + }); +});