fix(r): bind roxygen @param types for dotted formal names

parseRoxygenParams read the parameter name with `\w+`, which stops at the first
dot, so `#' @param na.rm Data` documented nothing and the formal got no type
(the old unit test pinned that as "does not bind a dotted parameter name").

Reproduction: four new cases in type-env-r.test.ts (na.rm, .data,
max_n.rows, braced form with a continuation line) returned undefined instead of
'Data' on the unfixed pattern. The name pattern is now `[\w.]*\w[\w.]*`, so a
dotted identifier matches while `...` and `x,y` stay unbound; the former
"does not bind" case is replaced by positive cases plus a not-a-formal control.
This commit is contained in:
Mick Cooney 2026-10-15 20:43:34 +01:00
parent f80cfa09f2
commit 725f4f511c
2 changed files with 48 additions and 4 deletions

View file

@ -66,8 +66,9 @@ const collectRoxygenBlock = (node: SyntaxNode): string => {
/**
* Split a roxygen block into its `@param` tags. A tag's description is the
* rest of its line plus every following line up to the next `@tag`, joined by
* single spaces. A tag whose name is not one plain identifier (`x,y`, `...`,
* `na.rm`) is skipped, and its continuation lines belong to no tag.
* single spaces. A tag whose name is not one identifier (`x,y`, `...`) is skipped,
* and its continuation lines belong to no tag. Dots are part of an R name
* (`na.rm`, `.data`).
*/
const parseRoxygenParams = (block: string): RoxygenParam[] => {
const params: RoxygenParam[] = [];
@ -85,7 +86,7 @@ const parseRoxygenParams = (block: string): RoxygenParam[] => {
if (tag) {
flush();
if (tag[1] !== 'param') continue;
const head = /^(\w+)(?:[ \t]+(.*))?$/.exec(tag[2] ?? '');
const head = /^([\w.]*\w[\w.]*)(?:[ \t]+(.*))?$/.exec(tag[2] ?? '');
if (!head) continue;
name = head[1];
if (head[2]) parts.push(head[2]);

View file

@ -509,13 +509,56 @@ save <- function(repo, backup) {
});
});
describe('dotted formal names', () => {
it.each([
['na.rm', 'na.rm'],
['a leading dot', '.data'],
['dots and underscores', 'max_n.rows'],
])('binds the documented type of %s', (_label, formal) => {
const tree = parse(`
Data <- R6::R6Class("Data", public = list(fit = function() 1))
#' @param ${formal} Data
#' @param other Data
run <- function(${formal}, other) {
${formal}
}
`);
const typeEnv = buildTypeEnv(tree, SupportedLanguages.R);
expect(flatGet(typeEnv, formal)).toBe('Data');
expect(flatGet(typeEnv, 'other')).toBe('Data');
});
it('keeps the braced form and the continuation lines of a dotted name', () => {
const tree = parse(`
Data <- R6::R6Class("Data", public = list(fit = function() 1))
#' @param na.rm {Data}
#' second line of prose
run <- function(na.rm) na.rm
`);
expect(flatGet(buildTypeEnv(tree, SupportedLanguages.R), 'na.rm')).toBe('Data');
});
it('binds nothing for a dotted name the function does not have', () => {
const tree = parse(`
Data <- R6::R6Class("Data", public = list(fit = function() 1))
#' @param na.rm Data
run <- function(x) x
`);
const typeEnv = buildTypeEnv(tree, SupportedLanguages.R);
expect(flatGet(typeEnv, 'na.rm')).toBeUndefined();
expect(flatGet(typeEnv, 'x')).toBeUndefined();
});
});
describe('forms that are not bound (guards)', () => {
it.each([
['\\code{Data} markup', "#' @param df \\code{Data}", ['df']],
['\\linkS4class{Data} markup', "#' @param df \\linkS4class{Data}", ['df']],
['a comma-separated name list', "#' @param x,y Data", ['x', 'y']],
['the dots parameter', "#' @param ... Data", ['...']],
['a dotted parameter name', "#' @param na.rm Data", ['na.rm']],
])('does not bind %s', (_label, tag, names) => {
const tree = parse(`
Data <- R6::R6Class("Data", public = list(fit = function() 1))