From 9c2c3ed595e54405fd0eba3111e32d2e7753129d Mon Sep 17 00:00:00 2001 From: Mick Cooney Date: Thu, 15 Oct 2026 20:33:36 +0100 Subject: [PATCH] docs(r): document that namespace-qualified S4 constructors are not recognised Review finding: `methods::setClass(...)` is ignored by the S4 field detector. Confirmed by test, and wider than fields: every S4/RefClass pattern (setClass, setRefClass, setGeneric, setMethod in R_QUERIES, plus the field and method extractors) requires a bare identifier callee, so a namespace_operator callee creates no class, slot, generic or method. R6::R6Class is supported. Widening touches nine files (queries, naming-argument, captures, owner hooks, post-parse, scope resolver, both extractors) and the qualifier-veto logic, so it is not a trivial change. Documented as a known limit in docs/languages/r-provider.md and pinned by characterisation tests in r-definition-anchors.test.ts (each paired with its unqualified control). --- docs/languages/r-provider.md | 1 + .../test/unit/r-definition-anchors.test.ts | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/docs/languages/r-provider.md b/docs/languages/r-provider.md index bffa86796..0f19555ab 100644 --- a/docs/languages/r-provider.md +++ b/docs/languages/r-provider.md @@ -86,6 +86,7 @@ Not supported: - Roxygen `@param` types live in the per-file type environment; no graph edge consumes them. - Argument matching follows R for named and reordered arguments but does not model partial argument names: `setClass(Cla = "D")`, `new(Cla = "E")`, `setRefClass(Cla = "A")` and `setMethod("show", sig = "Foo")` are not recognised, so no definition or owner is attached. - Members are read only from the named forms. The positional second argument of `setClass("A", representation(x = "numeric"))`, `R6Class("X", list(...))` and `setRefClass("A", list(...))` creates no field or slot nodes; use `slots=` / `representation=`, `public =` / `private =` / `active =` and `fields =`. +- S4 and RefClass constructors are recognised only as a bare callee: `setClass(...)`, `setRefClass(...)`, `setGeneric(...)` and `setMethod(...)`. A namespace-qualified call such as `methods::setClass("A", slots = list(x = "numeric"))` creates no class, slot, generic or method. `R6::R6Class(...)` is recognised. - For `S3method(generic, class, method)` the positional third argument names the exported method. A named third argument such as `S3method(print, foo, method = impl)` is not read: R registers `impl`, but the export is recorded for `print.foo`. - A leading `]` in a bracket expression of a NAMESPACE `exportPattern()` (for example `[]a]` or `[^]a]`) is read with JavaScript RegExp semantics, not POSIX/TRE, unless the pattern also contains a POSIX class such as `[:digit:]`; R would treat that `]` as a literal member. diff --git a/gitnexus/test/unit/r-definition-anchors.test.ts b/gitnexus/test/unit/r-definition-anchors.test.ts index 2c61499ce..181245751 100644 --- a/gitnexus/test/unit/r-definition-anchors.test.ts +++ b/gitnexus/test/unit/r-definition-anchors.test.ts @@ -14,6 +14,8 @@ 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'; +import { RFieldExtractor } from '../../src/core/ingestion/field-extractors/r.js'; +import { getRTopLevelMethodOwnerName } from '../../src/core/ingestion/method-extractors/r.js'; import { isRNonNamingArgumentCapture } from '../../src/core/ingestion/languages/r/naming-argument.js'; import { parseSourceSafe } from '../../src/core/tree-sitter/safe-parse.js'; import { MAX_RATIO, SHAPES, measureScaling } from '../helpers/r-argument-scaling.js'; @@ -317,6 +319,62 @@ describe('R_QUERIES: documented argument-matching limits', () => { }); }); +/** + * Documented limit (docs/languages/r-provider.md, Known limitations): the S4 and + * RefClass constructors and `setGeneric` / `setMethod` are recognised only as a bare + * identifier callee. A namespace-qualified call (`methods::setClass(...)`) has a + * `namespace_operator` callee, so it creates no class, slot, generic or method, in the + * query and in the field and method extractors alike. `R6::R6Class` is supported. + * Each case is paired with its unqualified positive control. + */ +describe('R_QUERIES: namespace-qualified S4 constructors are not recognised (documented)', () => { + it.each([ + [ + 'setClass', + 'setClass("A", slots = list(x = "numeric"))', + ['definition.class:A', 'definition.property:x'], + ], + [ + 'setRefClass', + 'setRefClass("A", fields = list(x = "numeric"))', + ['definition.class:A', 'definition.property:x'], + ], + [ + 'setGeneric', + 'setGeneric("area", function(shape) standardGeneric("area"))', + ['definition.function:area'], + ], + ['setMethod', 'setMethod("area", "Circle", function(shape) 1)', ['definition.method:area']], + ])('%s: bare call is recognised, methods::-qualified call is not', (name, bare, expected) => { + expect(definitions(bare)).toEqual(expected); + const qualified = bare.replace(name, `methods::${name}`); + expect(definitions(qualified)).toEqual([]); + }); + + it('the field and method extractors also require a bare identifier callee', () => { + const callOf = (source: string): Parser.SyntaxNode => + parseR(source).rootNode.descendantsOfType('call')[0]; + const fields = new RFieldExtractor(); + expect(fields.isTypeDeclaration(callOf('setClass("A", slots = list(x = "numeric"))'))).toBe( + true, + ); + expect( + fields.isTypeDeclaration(callOf('methods::setClass("A", slots = list(x = "numeric"))')), + ).toBe(false); + expect(getRTopLevelMethodOwnerName(callOf('setMethod("f", "A", function(x) 1)'))).toBe('A'); + expect(getRTopLevelMethodOwnerName(callOf('methods::setMethod("f", "A", function(x) 1)'))).toBe( + null, + ); + }); + + it('R6::R6Class, by contrast, is recognised', () => { + expect(definitions('X <- R6::R6Class("X", public = list(f = 1))')).toEqual([ + 'definition.class:X', + 'definition.property:f', + ]); + }); +}); + /** * Selecting the naming argument must stay linear in the argument count: an earlier * query-level formulation (a repeated sibling group before the argument) was