mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-15 04:18:05 +00:00
fix(r): bind a roxygen @param type only from an anchored form naming a local class
A @param description is free prose, but the first word of any capitalised description was bound as the parameter's type (`@param df Data frame ...` gave `df: Data`). A type is now bound only when the description is the type alone or starts with it in braces, and the type names an R6, S4 or RefClass class defined in the same file. Tags are read only at the start of a roxygen line and a description runs to the next tag. Remove the unwired extractReturnType, which took the first word of @return the same way.
This commit is contained in:
parent
82084db7a4
commit
165cf12508
8 changed files with 339 additions and 98 deletions
|
|
@ -708,7 +708,7 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas
|
|||
|
||||
**Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics
|
||||
|
||||
**R (experimental)** — `.r` / `.R` files, parsed with `@eagleoutice/tree-sitter-r`. Extracts functions (S3 methods appear as dotted-name functions such as `print.foo`), S4 classes (`setClass`, `setGeneric`, `setMethod`, slots), R6 classes with their methods and fields, R5 (`setRefClass`) methods, and roxygen2 `@param` types. The scope-resolution pipeline resolves `library()` / `require()` / `source()` imports to local packages and files, binds NAMESPACE `importFrom()` names to definitions in local packages, resolves `obj$method()` calls (receiver type inferred from `Type$new()`), follows calls inside native pipe (`|>`) chains, and links R6 `inherit=` / S4 `contains=` heritage. Export status is read from each local package's NAMESPACE (a `DESCRIPTION` marks the package): `export()`, `exportClasses()`, `exportMethods()`, `S3method()` and `exportPattern()` (multi-line directives, R string escapes and POSIX character classes handled) decide which symbols keep `isExported`; the language-level export check treats every symbol as public, and the NAMESPACE pass then marks the rest of the `R/` symbols of a NAMESPACE-bearing package non-exported (symbols outside `R/`, such as `tests/`, `vignettes/` and `inst/`, are not package members and stay public; files in `R/` subdirectories are treated as `R/` files although R itself does not load them). Packages without a NAMESPACE stay fully public and a nested package without one does not inherit its enclosing package's; roxygen2 `@export` tags are not consulted. An index of an R repository built before these rules needs a re-analyse to pick them up: export status (outside `R/`, and inside `R/` for a nested package without a NAMESPACE), `library()` of a root-level package (now linked), and `foo::f()` where `foo` is only the directory name of a package with a different `Package:` name (the call loses its name-based edge when package discovery was not truncated, or binds to a definition that duplicate definitions had previously dropped it for). Known limits: a bare call to a function in another file of the same package binds through a workspace-wide unique-name fallback at confidence 0.5 (`global-name-fallback`), not package-namespace lookup, and an ambiguous name yields no edge; dotted (S3-style) names are never import-resolved and bind by qualified-name lookup at 0.35; a qualified call `pkg::name()` is decided by its qualifier: when `pkg` is a package of the repository and defines `name` exactly once, the call binds to that definition at confidence 0.85 (`import-resolved`); when `pkg` is certainly not a repository package (package discovery completed and found no such package, including a repository with no `DESCRIPTION` at all), the call produces no edge; when `pkg` defines `name` more than once the call produces no edge; and when discovery may have missed a package (its depth and directory limits were reached and an unscanned subtree may hold an undiscovered package) the call keeps the name-based behaviour. Remaining gaps: a repository package that does not itself define `name` (for example a re-export) keeps the name-based behaviour; `pkg::name` used as a value rather than called (`lapply(x, pkg::name)`) is not resolved; a `::` to a name the package does not export still binds to its definition; and packages are discovered only to a bounded depth and directory count, so a package outside those limits is treated like an unknown one, while directories named exactly `renv`, `packrat`, `revdep` or `<name>.Rcheck` (installed libraries and check output) are never searched, so a package found only there is not a repository package; `import()`, `importClassesFrom()` and `importMethodsFrom()` are not read; `exportPattern()` character classes use ASCII semantics; S4 `contains=` links only to classes defined in the repository or the `VIRTUAL` marker, so basic-type bases add no edge; imports of external (CRAN) packages produce no edges; an edit to NAMESPACE alone is not re-resolved by an incremental re-index, so run a full analyze after changing it; no framework or entry-point detection.
|
||||
**R (experimental)** — `.r` / `.R` files, parsed with `@eagleoutice/tree-sitter-r`. Extracts functions (S3 methods appear as dotted-name functions such as `print.foo`), S4 classes (`setClass`, `setGeneric`, `setMethod`, slots), R6 classes with their methods and fields, R5 (`setRefClass`) methods, and roxygen2 `@param` types (when the description is a class defined in the same file, alone or in braces). The scope-resolution pipeline resolves `library()` / `require()` / `source()` imports to local packages and files, binds NAMESPACE `importFrom()` names to definitions in local packages, resolves `obj$method()` calls (receiver type inferred from `Type$new()`), follows calls inside native pipe (`|>`) chains, and links R6 `inherit=` / S4 `contains=` heritage. Export status is read from each local package's NAMESPACE (a `DESCRIPTION` marks the package): `export()`, `exportClasses()`, `exportMethods()`, `S3method()` and `exportPattern()` (multi-line directives, R string escapes and POSIX character classes handled) decide which symbols keep `isExported`; the language-level export check treats every symbol as public, and the NAMESPACE pass then marks the rest of the `R/` symbols of a NAMESPACE-bearing package non-exported (symbols outside `R/`, such as `tests/`, `vignettes/` and `inst/`, are not package members and stay public; files in `R/` subdirectories are treated as `R/` files although R itself does not load them). Packages without a NAMESPACE stay fully public and a nested package without one does not inherit its enclosing package's; roxygen2 `@export` tags are not consulted. An index of an R repository built before these rules needs a re-analyse to pick them up: export status (outside `R/`, and inside `R/` for a nested package without a NAMESPACE), `library()` of a root-level package (now linked), and `foo::f()` where `foo` is only the directory name of a package with a different `Package:` name (the call loses its name-based edge when package discovery was not truncated, or binds to a definition that duplicate definitions had previously dropped it for). Known limits: a bare call to a function in another file of the same package binds through a workspace-wide unique-name fallback at confidence 0.5 (`global-name-fallback`), not package-namespace lookup, and an ambiguous name yields no edge; dotted (S3-style) names are never import-resolved and bind by qualified-name lookup at 0.35; a qualified call `pkg::name()` is decided by its qualifier: when `pkg` is a package of the repository and defines `name` exactly once, the call binds to that definition at confidence 0.85 (`import-resolved`); when `pkg` is certainly not a repository package (package discovery completed and found no such package, including a repository with no `DESCRIPTION` at all), the call produces no edge; when `pkg` defines `name` more than once the call produces no edge; and when discovery may have missed a package (its depth and directory limits were reached and an unscanned subtree may hold an undiscovered package) the call keeps the name-based behaviour. Remaining gaps: a repository package that does not itself define `name` (for example a re-export) keeps the name-based behaviour; `pkg::name` used as a value rather than called (`lapply(x, pkg::name)`) is not resolved; a `::` to a name the package does not export still binds to its definition; and packages are discovered only to a bounded depth and directory count, so a package outside those limits is treated like an unknown one, while directories named exactly `renv`, `packrat`, `revdep` or `<name>.Rcheck` (installed libraries and check output) are never searched, so a package found only there is not a repository package; `import()`, `importClassesFrom()` and `importMethodsFrom()` are not read; `exportPattern()` character classes use ASCII semantics; S4 `contains=` links only to classes defined in the repository or the `VIRTUAL` marker, so basic-type bases add no edge; imports of external (CRAN) packages produce no edges; an edit to NAMESPACE alone is not re-resolved by an incremental re-index, so run a full analyze after changing it; no framework or entry-point detection.
|
||||
|
||||
**Control flow (CFG, opt-in `--pdg`)** — per-function control-flow graphs (`BasicBlock` nodes + `CFG` edges) feeding the PDG/taint substrate, currently **TypeScript & JavaScript** (#2081 M1); other languages planned. Off by default.
|
||||
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift,
|
|||
|
||||
**Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics
|
||||
|
||||
**R (experimental)** — `.r` / `.R` files, parsed with `@eagleoutice/tree-sitter-r`. Extracts functions (S3 methods appear as dotted-name functions such as `print.foo`), S4 classes (`setClass`, `setGeneric`, `setMethod`, slots), R6 classes with their methods and fields, R5 (`setRefClass`) methods, and roxygen2 `@param` types. The scope-resolution pipeline resolves `library()` / `require()` / `source()` imports to local packages and files, binds NAMESPACE `importFrom()` names to definitions in local packages, resolves `obj$method()` calls (receiver type inferred from `Type$new()`), follows calls inside native pipe (`|>`) chains, and links R6 `inherit=` / S4 `contains=` heritage. Export status is read from each local package's NAMESPACE (a `DESCRIPTION` marks the package): `export()`, `exportClasses()`, `exportMethods()`, `S3method()` and `exportPattern()` (multi-line directives, R string escapes and POSIX character classes handled) decide which symbols keep `isExported`; the language-level export check treats every symbol as public, and the NAMESPACE pass then marks the rest of the `R/` symbols of a NAMESPACE-bearing package non-exported (symbols outside `R/`, such as `tests/`, `vignettes/` and `inst/`, are not package members and stay public; files in `R/` subdirectories are treated as `R/` files although R itself does not load them). Packages without a NAMESPACE stay fully public and a nested package without one does not inherit its enclosing package's; roxygen2 `@export` tags are not consulted. An index of an R repository built before these rules needs a re-analyse to pick them up: export status (outside `R/`, and inside `R/` for a nested package without a NAMESPACE), `library()` of a root-level package (now linked), and `foo::f()` where `foo` is only the directory name of a package with a different `Package:` name (the call loses its name-based edge when package discovery was not truncated, or binds to a definition that duplicate definitions had previously dropped it for). Known limits: a bare call to a function in another file of the same package binds through a workspace-wide unique-name fallback at confidence 0.5 (`global-name-fallback`), not package-namespace lookup, and an ambiguous name yields no edge; dotted (S3-style) names are never import-resolved and bind by qualified-name lookup at 0.35; a qualified call `pkg::name()` is decided by its qualifier: when `pkg` is a package of the repository and defines `name` exactly once, the call binds to that definition at confidence 0.85 (`import-resolved`); when `pkg` is certainly not a repository package (package discovery completed and found no such package, including a repository with no `DESCRIPTION` at all), the call produces no edge; when `pkg` defines `name` more than once the call produces no edge; and when discovery may have missed a package (its depth and directory limits were reached and an unscanned subtree may hold an undiscovered package) the call keeps the name-based behaviour. Remaining gaps: a repository package that does not itself define `name` (for example a re-export) keeps the name-based behaviour; `pkg::name` used as a value rather than called (`lapply(x, pkg::name)`) is not resolved; a `::` to a name the package does not export still binds to its definition; and packages are discovered only to a bounded depth and directory count, so a package outside those limits is treated like an unknown one, while directories named exactly `renv`, `packrat`, `revdep` or `<name>.Rcheck` (installed libraries and check output) are never searched, so a package found only there is not a repository package; `import()`, `importClassesFrom()` and `importMethodsFrom()` are not read; `exportPattern()` character classes use ASCII semantics; S4 `contains=` links only to classes defined in the repository or the `VIRTUAL` marker, so basic-type bases add no edge; imports of external (CRAN) packages produce no edges; an edit to NAMESPACE alone is not re-resolved by an incremental re-index, so run a full analyze after changing it; no framework or entry-point detection.
|
||||
**R (experimental)** — `.r` / `.R` files, parsed with `@eagleoutice/tree-sitter-r`. Extracts functions (S3 methods appear as dotted-name functions such as `print.foo`), S4 classes (`setClass`, `setGeneric`, `setMethod`, slots), R6 classes with their methods and fields, R5 (`setRefClass`) methods, and roxygen2 `@param` types (when the description is a class defined in the same file, alone or in braces). The scope-resolution pipeline resolves `library()` / `require()` / `source()` imports to local packages and files, binds NAMESPACE `importFrom()` names to definitions in local packages, resolves `obj$method()` calls (receiver type inferred from `Type$new()`), follows calls inside native pipe (`|>`) chains, and links R6 `inherit=` / S4 `contains=` heritage. Export status is read from each local package's NAMESPACE (a `DESCRIPTION` marks the package): `export()`, `exportClasses()`, `exportMethods()`, `S3method()` and `exportPattern()` (multi-line directives, R string escapes and POSIX character classes handled) decide which symbols keep `isExported`; the language-level export check treats every symbol as public, and the NAMESPACE pass then marks the rest of the `R/` symbols of a NAMESPACE-bearing package non-exported (symbols outside `R/`, such as `tests/`, `vignettes/` and `inst/`, are not package members and stay public; files in `R/` subdirectories are treated as `R/` files although R itself does not load them). Packages without a NAMESPACE stay fully public and a nested package without one does not inherit its enclosing package's; roxygen2 `@export` tags are not consulted. An index of an R repository built before these rules needs a re-analyse to pick them up: export status (outside `R/`, and inside `R/` for a nested package without a NAMESPACE), `library()` of a root-level package (now linked), and `foo::f()` where `foo` is only the directory name of a package with a different `Package:` name (the call loses its name-based edge when package discovery was not truncated, or binds to a definition that duplicate definitions had previously dropped it for). Known limits: a bare call to a function in another file of the same package binds through a workspace-wide unique-name fallback at confidence 0.5 (`global-name-fallback`), not package-namespace lookup, and an ambiguous name yields no edge; dotted (S3-style) names are never import-resolved and bind by qualified-name lookup at 0.35; a qualified call `pkg::name()` is decided by its qualifier: when `pkg` is a package of the repository and defines `name` exactly once, the call binds to that definition at confidence 0.85 (`import-resolved`); when `pkg` is certainly not a repository package (package discovery completed and found no such package, including a repository with no `DESCRIPTION` at all), the call produces no edge; when `pkg` defines `name` more than once the call produces no edge; and when discovery may have missed a package (its depth and directory limits were reached and an unscanned subtree may hold an undiscovered package) the call keeps the name-based behaviour. Remaining gaps: a repository package that does not itself define `name` (for example a re-export) keeps the name-based behaviour; `pkg::name` used as a value rather than called (`lapply(x, pkg::name)`) is not resolved; a `::` to a name the package does not export still binds to its definition; and packages are discovered only to a bounded depth and directory count, so a package outside those limits is treated like an unknown one, while directories named exactly `renv`, `packrat`, `revdep` or `<name>.Rcheck` (installed libraries and check output) are never searched, so a package found only there is not a repository package; `import()`, `importClassesFrom()` and `importMethodsFrom()` are not read; `exportPattern()` character classes use ASCII semantics; S4 `contains=` links only to classes defined in the repository or the `VIRTUAL` marker, so basic-type bases add no edge; imports of external (CRAN) packages produce no edges; an edit to NAMESPACE alone is not re-resolved by an incremental re-index, so run a full analyze after changing it; no framework or entry-point detection.
|
||||
|
||||
## Agent Skills
|
||||
|
||||
|
|
|
|||
|
|
@ -10,29 +10,35 @@ import type { SyntaxNode } from '../utils/ast-helpers.js';
|
|||
/**
|
||||
* R type extractor — roxygen2 annotation parsing.
|
||||
*
|
||||
* R has no static type system, but the roxygen2 documentation convention
|
||||
* provides de facto type annotations via comments:
|
||||
* R has no static type system and roxygen2 has no type syntax: a `@param`
|
||||
* description is free prose. A parameter is therefore typed from a roxygen
|
||||
* comment only when the type is stated in an anchored form AND names a class
|
||||
* the file defines:
|
||||
*
|
||||
* #' @param name Character the user's name
|
||||
* #' @param repo UserRepo the repository
|
||||
* #' @return User
|
||||
* create <- function(name, repo) {
|
||||
* repo$save()
|
||||
* }
|
||||
* #' @param repo UserRepo (the description is the type alone)
|
||||
* #' @param backup {UserRepo} a spare (the type in braces, prose after it)
|
||||
* save <- function(repo, backup) { ... }
|
||||
*
|
||||
* This extractor parses `#' @param name Type` patterns from comment nodes
|
||||
* preceding function definitions and binds parameter names to their types.
|
||||
* Prose that merely starts with a capitalised word (`@param df Data frame of
|
||||
* observations`) binds nothing, even when a class called `Data` exists, and a
|
||||
* description may run over several lines: it is read up to the next tag.
|
||||
*
|
||||
* Resolution tiers:
|
||||
* - Tier 0: roxygen2 @param annotations (extractDeclaration pre-populates env)
|
||||
* - Tier 1: Constructor inference via `obj <- ClassName$new()` (R6) or `obj <- new("ClassName")` (S4)
|
||||
*/
|
||||
|
||||
/** Regex to extract @param annotations: `#' @param name Type description` */
|
||||
const ROXYGEN_PARAM_RE = /#'\s*@param\s+(\w+)\s+(\S+)/g;
|
||||
/** A `@param` tag: the parameter name and the description text that follows it. */
|
||||
interface RoxygenParam {
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
/** Regex to extract @return annotations: `#' @return Type` */
|
||||
const ROXYGEN_RETURN_RE = /#'\s*@return\s+(\S+)/;
|
||||
/** The whole description is a single type word: `UserRepo`. */
|
||||
const BARE_TYPE_RE = /^([A-Z][\w.]*)$/;
|
||||
|
||||
/** The description starts with a braced type: `{UserRepo} the repository`. */
|
||||
const BRACED_TYPE_RE = /^\{([A-Z][\w.]*)\}(?:\s|$)/;
|
||||
|
||||
/**
|
||||
* Walk backwards through preceding sibling nodes collecting consecutive
|
||||
|
|
@ -55,24 +61,95 @@ const collectRoxygenBlock = (node: SyntaxNode): string => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Collect roxygen2 @param annotations from comment nodes preceding a function definition.
|
||||
* 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.
|
||||
*/
|
||||
const parseRoxygenParams = (block: string): RoxygenParam[] => {
|
||||
const params: RoxygenParam[] = [];
|
||||
let name: string | undefined;
|
||||
let parts: string[] = [];
|
||||
const flush = (): void => {
|
||||
if (name !== undefined) params.push({ name, description: parts.join(' ').trim() });
|
||||
name = undefined;
|
||||
parts = [];
|
||||
};
|
||||
|
||||
for (const rawLine of block.split('\n')) {
|
||||
const line = rawLine.replace(/^#'[ \t]*/, '').trim();
|
||||
const tag = /^@(\w+)(?:[ \t]+(.*))?$/.exec(line);
|
||||
if (tag) {
|
||||
flush();
|
||||
if (tag[1] !== 'param') continue;
|
||||
const head = /^(\w+)(?:[ \t]+(.*))?$/.exec(tag[2] ?? '');
|
||||
if (!head) continue;
|
||||
name = head[1];
|
||||
if (head[2]) parts.push(head[2]);
|
||||
} else if (name !== undefined && line.length > 0) {
|
||||
parts.push(line);
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return params;
|
||||
};
|
||||
|
||||
/** The type a `@param` description states in an anchored form, if any. */
|
||||
const anchoredType = (description: string): string | undefined =>
|
||||
(BARE_TYPE_RE.exec(description) ?? BRACED_TYPE_RE.exec(description))?.[1];
|
||||
|
||||
/**
|
||||
* The class a node declares, in the shapes `query.ts` captures as
|
||||
* `@declaration.class`: `X <- R6Class(...)` / `X <- R6::R6Class(...)`, and
|
||||
* `setClass("X", ...)` / `setRefClass("X", ...)`.
|
||||
*/
|
||||
const declaredClassName = (node: SyntaxNode): string | undefined => {
|
||||
if (node.type === 'binary_operator') {
|
||||
const lhs = node.childForFieldName('lhs');
|
||||
const rhs = node.childForFieldName('rhs');
|
||||
if (lhs?.type !== 'identifier' || rhs?.type !== 'call') return undefined;
|
||||
const fn = rhs.childForFieldName('function');
|
||||
const calleeName =
|
||||
fn?.type === 'namespace_operator' ? fn.childForFieldName('rhs')?.text : fn?.text;
|
||||
return calleeName === 'R6Class' ? lhs.text : undefined;
|
||||
}
|
||||
if (node.type !== 'call') return undefined;
|
||||
const fn = node.childForFieldName('function');
|
||||
if (fn?.type !== 'identifier' || (fn.text !== 'setClass' && fn.text !== 'setRefClass')) {
|
||||
return undefined;
|
||||
}
|
||||
const first = node
|
||||
.childForFieldName('arguments')
|
||||
?.namedChildren.find((c) => c.type === 'argument');
|
||||
const value = first?.childForFieldName('value');
|
||||
if (value?.type !== 'string') return undefined;
|
||||
return value.namedChildren.find((c) => c.type === 'string_content')?.text;
|
||||
};
|
||||
|
||||
/** Class names defined anywhere in the file, built on first use and kept per tree. */
|
||||
const localClassNamesByTree = new WeakMap<object, ReadonlySet<string>>();
|
||||
const localClassNames = (node: SyntaxNode): ReadonlySet<string> => {
|
||||
const cached = localClassNamesByTree.get(node.tree);
|
||||
if (cached) return cached;
|
||||
const names = new Set<string>();
|
||||
for (const candidate of node.tree.rootNode.descendantsOfType(['binary_operator', 'call'])) {
|
||||
const name = declaredClassName(candidate);
|
||||
if (name !== undefined) names.add(name);
|
||||
}
|
||||
localClassNamesByTree.set(node.tree, names);
|
||||
return names;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect the roxygen2 `@param` types of a function definition.
|
||||
* Returns a map of paramName → typeName.
|
||||
*/
|
||||
const collectRoxygenParams = (node: SyntaxNode): Map<string, string> => {
|
||||
const params = new Map<string, string>();
|
||||
const commentBlock = collectRoxygenBlock(node);
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
ROXYGEN_PARAM_RE.lastIndex = 0;
|
||||
while ((match = ROXYGEN_PARAM_RE.exec(commentBlock)) !== null) {
|
||||
const paramName = match[1];
|
||||
const typeName = match[2];
|
||||
// Only accept types that start with uppercase (class/type names)
|
||||
if (/^[A-Z]/.test(typeName)) {
|
||||
params.set(paramName, typeName);
|
||||
}
|
||||
for (const { name, description } of parseRoxygenParams(collectRoxygenBlock(node))) {
|
||||
const typeName = anchoredType(description);
|
||||
if (typeName !== undefined && localClassNames(node).has(typeName)) params.set(name, typeName);
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
|
|
@ -185,20 +262,6 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
|
|||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract return type from roxygen2 `#' @return Type` annotation preceding
|
||||
* a function definition. Walks backwards through preceding sibling comment nodes.
|
||||
*/
|
||||
export const extractReturnType = (node: SyntaxNode): string | undefined => {
|
||||
const commentBlock = collectRoxygenBlock(node);
|
||||
const match = ROXYGEN_RETURN_RE.exec(commentBlock);
|
||||
if (match) {
|
||||
const typeName = match[1];
|
||||
if (/^[A-Z]/.test(typeName)) return typeName;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
||||
extractDeclaration,
|
||||
|
|
|
|||
5
gitnexus/test/fixtures/lang-resolution/r-roxygen-param-prose/R/model.R
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/r-roxygen-param-prose/R/model.R
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Data <- R6::R6Class("Data",
|
||||
public = list(
|
||||
fit = function() 1
|
||||
)
|
||||
)
|
||||
12
gitnexus/test/fixtures/lang-resolution/r-roxygen-param-prose/R/run.R
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/r-roxygen-param-prose/R/run.R
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#' Fit a model.
|
||||
#'
|
||||
#' @param df Data frame of observations
|
||||
fit_from_prose <- function(df) {
|
||||
df$fit()
|
||||
}
|
||||
|
||||
#' Fit a model from a freshly built object.
|
||||
fit_from_constructor <- function() {
|
||||
d <- Data$new()
|
||||
d$fit()
|
||||
}
|
||||
27
gitnexus/test/integration/resolvers/r-roxygen-param.test.ts
Normal file
27
gitnexus/test/integration/resolvers/r-roxygen-param.test.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* R: a roxygen `@param` description is prose, not a type. The type
|
||||
* environment is the only place a roxygen type is held, and no graph edge
|
||||
* consumes it, so this pins the end-to-end outcome: a function documented
|
||||
* `@param df Data frame ...` is not linked to the unrelated R6 class `Data`.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import { FIXTURES, getRelationships, runPipelineFromRepo, type PipelineResult } from './helpers.js';
|
||||
|
||||
describe('R roxygen @param prose does not type a receiver', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'r-roxygen-param-prose'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('links a receiver typed by a constructor call to the class method (positive control)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
expect(calls.some((e) => e.source === 'fit_from_constructor' && e.target === 'fit')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not link a receiver documented only by a prose description to the class method', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
expect(calls.filter((e) => e.source === 'fit_from_prose')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { buildTypeEnv } from '../../src/core/ingestion/type-env.js';
|
||||
import { extractReturnType as rExtractReturnType } from '../../src/core/ingestion/type-extractors/r.js';
|
||||
import Parser from 'tree-sitter';
|
||||
import R from '@eagleoutice/tree-sitter-r';
|
||||
|
||||
|
|
@ -29,8 +28,10 @@ describe('buildTypeEnv', () => {
|
|||
describe('R roxygen2 annotations', () => {
|
||||
it('extracts @param type bindings from roxygen2 comments', () => {
|
||||
const tree = parse(`
|
||||
#' @param data DataFrame the input data
|
||||
#' @param outliers DataFrame outlier records
|
||||
DataFrame <- R6::R6Class("DataFrame", public = list(rows = function() 1))
|
||||
|
||||
#' @param data DataFrame
|
||||
#' @param outliers DataFrame
|
||||
AddOutlierStatuses <- function(data, outliers) {
|
||||
data
|
||||
}
|
||||
|
|
@ -42,15 +43,15 @@ AddOutlierStatuses <- function(data, outliers) {
|
|||
|
||||
it('skips lowercase type names (primitive types)', () => {
|
||||
const tree = parse(`
|
||||
#' @param x numeric input value
|
||||
#' @param name Character the name
|
||||
#' @param x numeric
|
||||
#' @param name Character
|
||||
process <- function(x, name) {
|
||||
x
|
||||
}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'x')).toBeUndefined();
|
||||
expect(flatGet(typeEnv, 'name')).toBe('Character');
|
||||
expect(flatGet(typeEnv, 'name')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('extracts no types when no roxygen2 comments present', () => {
|
||||
|
|
@ -63,12 +64,16 @@ process <- function(x, y) {
|
|||
expect(flatSize(typeEnv)).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts types from = assignment function definitions', () => {
|
||||
it.each([
|
||||
['<-', 'helper <- function(repo) {\n repo\n}'],
|
||||
['=', 'helper = function(repo) {\n repo\n}'],
|
||||
['<<-', 'helper <<- function(repo) {\n repo\n}'],
|
||||
])('extracts types from %s function definitions', (_op, definition) => {
|
||||
const tree = parse(`
|
||||
#' @param repo UserRepo the repository
|
||||
helper = function(repo) {
|
||||
repo
|
||||
}
|
||||
UserRepo <- R6::R6Class("UserRepo", public = list(save = function() 1))
|
||||
|
||||
#' @param repo UserRepo
|
||||
${definition}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'repo')).toBe('UserRepo');
|
||||
|
|
@ -84,45 +89,6 @@ rs <- ResultSet$new(items)
|
|||
expect(binding!.calleeName).toBe('ResultSet');
|
||||
});
|
||||
|
||||
it('extractReturnType extracts @return type from roxygen2 comment', () => {
|
||||
const tree = parse(`
|
||||
#' @param name Character the user name
|
||||
#' @return User
|
||||
create <- function(name) {
|
||||
name
|
||||
}
|
||||
`);
|
||||
let defNode: any = null;
|
||||
for (let i = 0; i < tree.rootNode.childCount; i++) {
|
||||
const child = tree.rootNode.child(i);
|
||||
if (child?.type === 'binary_operator') {
|
||||
defNode = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(defNode).not.toBeNull();
|
||||
const returnType = rExtractReturnType(defNode);
|
||||
expect(returnType).toBe('User');
|
||||
});
|
||||
|
||||
it('extractReturnType skips lowercase @return types', () => {
|
||||
const tree = parse(`
|
||||
#' @return numeric
|
||||
compute <- function() { 42 }
|
||||
`);
|
||||
let defNode: any = null;
|
||||
for (let i = 0; i < tree.rootNode.childCount; i++) {
|
||||
const child = tree.rootNode.child(i);
|
||||
if (child?.type === 'binary_operator') {
|
||||
defNode = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(defNode).not.toBeNull();
|
||||
const returnType = rExtractReturnType(defNode);
|
||||
expect(returnType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns constructor binding for S4 obj <- new("ClassName")', () => {
|
||||
const tree = parse(`
|
||||
model <- new("DataModel", name = "test")
|
||||
|
|
@ -135,8 +101,11 @@ model <- new("DataModel", name = "test")
|
|||
|
||||
it('extracts @param types when @examples block is present', () => {
|
||||
const tree = parse(`
|
||||
#' @param data DataFrame the input data
|
||||
#' @param config Config configuration object
|
||||
DataFrame <- R6::R6Class("DataFrame", public = list(rows = function() 1))
|
||||
Config <- R6::R6Class("Config", public = list(get = function() 1))
|
||||
|
||||
#' @param data DataFrame
|
||||
#' @param config {Config} configuration object
|
||||
#' @return Result
|
||||
#' @examples
|
||||
#' result <- process(my_data, my_config)
|
||||
|
|
@ -152,12 +121,177 @@ process <- function(data, config) {
|
|||
|
||||
it('extracts @param types when regular comments appear before function', () => {
|
||||
const tree = parse(`
|
||||
#' @param x DataFrame the input
|
||||
DataFrame <- R6::R6Class("DataFrame", public = list(rows = function() 1))
|
||||
|
||||
#' @param x DataFrame
|
||||
# TODO: refactor this later
|
||||
compute <- function(x) { x }
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'x')).toBe('DataFrame');
|
||||
});
|
||||
|
||||
it('binds a parameter only when the tag names a class the file defines', () => {
|
||||
const tree = parse(`
|
||||
#' @param data DataFrame
|
||||
#' @param name Character
|
||||
process <- function(data, name) {
|
||||
data
|
||||
}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'data')).toBeUndefined();
|
||||
expect(flatGet(typeEnv, 'name')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not bind a lowercase word even when the file defines a class of that name', () => {
|
||||
const tree = parse(`
|
||||
setClass("repo", representation(name = "character"))
|
||||
|
||||
#' @param r repo
|
||||
save <- function(r) { r }
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'r')).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('description prose is not a type', () => {
|
||||
it('does not bind the first word of a description that names an R6 class', () => {
|
||||
const tree = parse(`
|
||||
Data <- R6::R6Class("Data", public = list(fit = function() 1))
|
||||
|
||||
#' @param df Data frame of observations
|
||||
run <- function(df) {
|
||||
df$fit()
|
||||
}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'df')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not bind the first word of a description that names an S4 class', () => {
|
||||
const tree = parse(`
|
||||
setClass("Config", representation(path = "character"))
|
||||
|
||||
#' @param cfg Config object to read from
|
||||
load <- function(cfg) {
|
||||
cfg
|
||||
}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'cfg')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not bind an article or determiner that happens to be a class name', () => {
|
||||
const tree = parse(`
|
||||
A <- R6::R6Class("A", public = list(go = function() 1))
|
||||
|
||||
#' @param x A numeric vector
|
||||
f <- function(x) { x }
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'x')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not bind from the first line of a description that continues on the next line', () => {
|
||||
const tree = parse(`
|
||||
Data <- R6::R6Class("Data", public = list(fit = function() 1))
|
||||
|
||||
#' @param df Data
|
||||
#' frame of observations
|
||||
run <- function(df) {
|
||||
df$fit()
|
||||
}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'df')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('binds a braced type whose prose continues on the next line', () => {
|
||||
const tree = parse(`
|
||||
Data <- R6::R6Class("Data", public = list(fit = function() 1))
|
||||
|
||||
#' @param df {Data} the observations,
|
||||
#' one row per case
|
||||
run <- function(df) {
|
||||
df$fit()
|
||||
}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'df')).toBe('Data');
|
||||
});
|
||||
});
|
||||
|
||||
it('reads a tag only at the start of a roxygen line', () => {
|
||||
const tree = parse(`
|
||||
UserRepo <- R6::R6Class("UserRepo", public = list(save = function() 1))
|
||||
|
||||
#' Write the tag as #' @param repo UserRepo in your own comments.
|
||||
save <- function(repo) {
|
||||
repo
|
||||
}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'repo')).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('class definition forms', () => {
|
||||
it.each([
|
||||
['bare R6Class', 'UserRepo <- R6Class("UserRepo", public = list(save = function() 1))'],
|
||||
[
|
||||
'namespaced R6::R6Class',
|
||||
'UserRepo <- R6::R6Class("UserRepo", public = list(save = function() 1))',
|
||||
],
|
||||
['setClass', 'setClass("UserRepo", representation(name = "character"))'],
|
||||
['setRefClass', 'setRefClass("UserRepo", fields = list(name = "character"))'],
|
||||
])('binds a type defined with %s', (_form, definition) => {
|
||||
const tree = parse(`
|
||||
#' @param repo UserRepo
|
||||
save <- function(repo) {
|
||||
repo
|
||||
}
|
||||
|
||||
${definition}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'repo')).toBe('UserRepo');
|
||||
});
|
||||
|
||||
it('binds the braced form, with or without prose after it', () => {
|
||||
const tree = parse(`
|
||||
UserRepo <- R6::R6Class("UserRepo", public = list(save = function() 1))
|
||||
|
||||
#' @param repo {UserRepo} the repository
|
||||
#' @param backup {UserRepo}
|
||||
save <- function(repo, backup) {
|
||||
repo
|
||||
}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
expect(flatGet(typeEnv, 'repo')).toBe('UserRepo');
|
||||
expect(flatGet(typeEnv, 'backup')).toBe('UserRepo');
|
||||
});
|
||||
});
|
||||
|
||||
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))
|
||||
|
||||
${tag}
|
||||
run <- function(x, y, df, ..., na.rm = FALSE) {
|
||||
x
|
||||
}
|
||||
`);
|
||||
const typeEnv = buildTypeEnv(tree, 'r');
|
||||
for (const name of names) expect(flatGet(typeEnv, name)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -388,7 +388,7 @@ So return-type-aware receiver inference already exists in a constrained downstre
|
|||
| Assignment chains | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | No |
|
||||
| Field/property type resolution | Yes | No† | Yes | Yes | Yes | Yes | Yes | Yes* | Yes | YARD | No | Yes | No‡ | No | Partial‖‖‖ |
|
||||
| Comment-based types | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No | No | Roxygen2 |
|
||||
| Return type extraction | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No | No | Roxygen2 |
|
||||
| Return type extraction | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No | No | No |
|
||||
| Call-result variable binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No | Yes | Yes |
|
||||
| Field access binding | Yes | No† | Yes | Yes | Yes | Yes | Yes | No‖ | Yes | N/A | Yes††† | Yes | No | Yes | No |
|
||||
| Method-call-result binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No | Yes | Yes |
|
||||
|
|
@ -431,7 +431,7 @@ So return-type-aware receiver inference already exists in a constrained downstre
|
|||
|
||||
\*\*\* Whole-module-import languages (Go, Ruby, C/C++, Swift, R): namedImportMap entries synthesized from graph-exported symbols via `synthesizeWildcardImportBindings()`. Not from import AST node extraction.
|
||||
|
||||
‖‖‖ R has no inline type annotations. Parameter types are extracted from roxygen2 `#' @param name Type` comments. Field/property types for S4 slots come from `representation(name = "character")` string literals; R6 fields infer types from default values (`NULL`, `TRUE`, `0L`, `"str"`).
|
||||
‖‖‖ R has no inline type annotations. Parameter types are extracted from roxygen2 `@param` tags only when the description is the type alone (`#' @param name Type`) or starts with it in braces (`#' @param name {Type} prose`) and `Type` is an R6, S4 or RefClass class defined in the same file; prose that merely begins with a capitalised word binds nothing. Roxygen `@return` is not read. Field/property types for S4 slots come from `representation(name = "character")` string literals; R6 fields infer types from default values (`NULL`, `TRUE`, `0L`, `"str"`).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue