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).
This commit is contained in:
Mick Cooney 2026-10-15 20:33:36 +01:00
parent 9c39aefdb6
commit 9c2c3ed595
2 changed files with 59 additions and 0 deletions

View file

@ -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.

View file

@ -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