From 3dbf1017ce958552bbe75e2dee18f0feef537a86 Mon Sep 17 00:00:00 2001 From: Mick Cooney Date: Thu, 15 Oct 2026 15:42:35 +0100 Subject: [PATCH] feat(r): add conservative semantic rename provider Plain R functions rename through the scope graph; S3, string-spelled, S4, R6 and NAMESPACE-exported names are refused. Adds fixtures, the rename-provider-r test and its cross-platform shard entries. --- docs/languages/r-provider.md | 14 +++ gitnexus/scripts/cross-platform-shard.ts | 1 + gitnexus/scripts/cross-platform-tests.ts | 1 + gitnexus/src/core/ingestion/languages/r.ts | 2 + .../src/core/ingestion/languages/r/rename.ts | 106 ++++++++++++++++++ .../r/function-call-chain/after/chain.R | 10 ++ .../r/function-call-chain/before/chain.R | 10 ++ .../r/function-call-chain/case.json | 15 +++ .../after/apply.R | 3 + .../function-unrelated-parameter/after/lib.R | 7 ++ .../before/apply.R | 3 + .../function-unrelated-parameter/before/lib.R | 7 ++ .../r/function-unrelated-parameter/case.json | 15 +++ .../r/invalid-name-invalid-start/after/lib.R | 3 + .../r/invalid-name-invalid-start/before/lib.R | 3 + .../r/invalid-name-invalid-start/case.json | 9 ++ .../r/invalid-name-reserved-word/after/lib.R | 3 + .../r/invalid-name-reserved-word/before/lib.R | 3 + .../r/invalid-name-reserved-word/case.json | 9 ++ .../r/namespace-export/after/DESCRIPTION | 3 + .../r/namespace-export/after/NAMESPACE | 1 + .../r/namespace-export/after/R/lib.R | 5 + .../r/namespace-export/before/DESCRIPTION | 3 + .../r/namespace-export/before/NAMESPACE | 1 + .../r/namespace-export/before/R/lib.R | 5 + .../r/namespace-export/case.json | 9 ++ .../r/r6-class/after/account.R | 11 ++ .../r/r6-class/before/account.R | 11 ++ .../rename-integration/r/r6-class/case.json | 9 ++ .../r/s3-generic/after/shapes.R | 5 + .../r/s3-generic/before/shapes.R | 5 + .../rename-integration/r/s3-generic/case.json | 9 ++ .../r/s3-method/after/shapes.R | 3 + .../r/s3-method/before/shapes.R | 3 + .../rename-integration/r/s3-method/case.json | 23 ++++ .../r/s3-new-method-name/after/shapes.R | 3 + .../r/s3-new-method-name/before/shapes.R | 3 + .../r/s3-new-method-name/case.json | 9 ++ .../r/s4-class/after/person.R | 3 + .../r/s4-class/before/person.R | 3 + .../rename-integration/r/s4-class/case.json | 9 ++ .../r/string-lookup/after/dyn.R | 3 + .../r/string-lookup/before/dyn.R | 3 + .../r/string-lookup/case.json | 9 ++ .../integration/rename-provider-r.test.ts | 3 + 45 files changed, 378 insertions(+) create mode 100644 gitnexus/src/core/ingestion/languages/r/rename.ts create mode 100644 gitnexus/test/fixtures/rename-integration/r/function-call-chain/after/chain.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/function-call-chain/before/chain.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/function-call-chain/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/after/apply.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/after/lib.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/before/apply.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/before/lib.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/after/lib.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/before/lib.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/after/lib.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/before/lib.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/namespace-export/after/DESCRIPTION create mode 100644 gitnexus/test/fixtures/rename-integration/r/namespace-export/after/NAMESPACE create mode 100644 gitnexus/test/fixtures/rename-integration/r/namespace-export/after/R/lib.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/namespace-export/before/DESCRIPTION create mode 100644 gitnexus/test/fixtures/rename-integration/r/namespace-export/before/NAMESPACE create mode 100644 gitnexus/test/fixtures/rename-integration/r/namespace-export/before/R/lib.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/namespace-export/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/r6-class/after/account.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/r6-class/before/account.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/r6-class/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/s3-generic/after/shapes.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/s3-generic/before/shapes.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/s3-generic/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/s3-method/after/shapes.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/s3-method/before/shapes.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/s3-method/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/after/shapes.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/before/shapes.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/s4-class/after/person.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/s4-class/before/person.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/s4-class/case.json create mode 100644 gitnexus/test/fixtures/rename-integration/r/string-lookup/after/dyn.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/string-lookup/before/dyn.R create mode 100644 gitnexus/test/fixtures/rename-integration/r/string-lookup/case.json create mode 100644 gitnexus/test/integration/rename-provider-r.test.ts diff --git a/docs/languages/r-provider.md b/docs/languages/r-provider.md index aad20613c9..bffa867963 100644 --- a/docs/languages/r-provider.md +++ b/docs/languages/r-provider.md @@ -111,6 +111,20 @@ Not modelled: - `stop()`, `on.exit()`, `tryCatch` handlers and other non-local exits are ordinary calls: no throw edges and no finaliser routing. A `break` / `next` with no enclosing loop falls back to EXIT. - Function join: a CFG is anchored at the `function_definition` token, while the `Function` node starts at the whole assignment. They share a start line for `f <- function`, `f = function` and `f <- \(x)`, but not for `f <-` followed by `function` on the next line; there the CFG is still emitted and its blocks lie inside the `Function` node's span, but the exact start-line join used by taint summaries does not resolve (inert for R today, since reaching definitions have no facts). Anonymous functions have a CFG but no `Function` node. +## Rename + +`languages/r/rename.ts` gives the `rename` tool a deliberately narrow, fail-closed R provider (fixtures under `test/fixtures/rename-integration/r/`). R binds names dynamically, so only what the scope graph proves is renamed. + +- **Succeeds:** a plain function (`name <- function(...)`) and the calls the graph resolves to it, in one file or across files. A same-named parameter is claimed as a binding and left alone. +- **Refused (`unsupported_occurrence`, planning status `unsupported`):** + - any target that is not a plain `Function` (S4 classes, R6 classes, members); + - a function whose old or new name is spelled as a string literal anywhere in the indexed R sources (`UseMethod("area")`, `do.call("f", ...)`, `match.fun`, `get`, `setGeneric`, ...), because string lookup is not resolved; + - a new name `generic.class` whose prefix is an existing function, which would register an S3 method; + - a package function under `R/` that `NAMESPACE` does not mark private (`isExported` is not false), because `NAMESPACE` is not an indexed source and its `export()` or `S3method()` entry would go stale. +- **Refused by the shared engine (fail-closed, not provider code):** S3 methods and any dotted function name (`area.circle` reads as an owner-qualified member, so `stale_graph`); S4 class names (`unsupported_occurrence`, no exact source-name span); R6 methods (`stale_graph`). +- **Invalid names (`invalid_name`):** anything that is not a nonreserved R identifier. +- **Not proven:** unqualified calls to a function defined in another file (including the package `R/` layout) fail with `ambiguous_reference`, because R's global-name fallback is not a provable binding. Renaming across files works only where the call is qualified or otherwise resolved. + ## Type annotations R has no inline type annotations, so the "Type Annotations" column of the language support table is not ticked for R. The roxygen2 `@param` extraction above is a comment-based fallback, in the same class as the JSDoc, PHPDoc and YARD fallbacks listed in type-resolution-system.md (Ruby's YARD support is likewise not ticked). diff --git a/gitnexus/scripts/cross-platform-shard.ts b/gitnexus/scripts/cross-platform-shard.ts index 0c9bc90995..07483169ee 100644 --- a/gitnexus/scripts/cross-platform-shard.ts +++ b/gitnexus/scripts/cross-platform-shard.ts @@ -62,6 +62,7 @@ export const WINDOWS_WEIGHTS_SEC: Readonly> = { 'test/integration/rename-provider-objective-c.test.ts': 285, 'test/integration/rename-provider-php.test.ts': 100, 'test/integration/rename-provider-python.test.ts': 375, + 'test/integration/rename-provider-r.test.ts': 100, 'test/integration/rename-provider-ruby.test.ts': 100, 'test/integration/rename-provider-rust.test.ts': 75, 'test/integration/rename-provider-swift.test.ts': 300, diff --git a/gitnexus/scripts/cross-platform-tests.ts b/gitnexus/scripts/cross-platform-tests.ts index 553f5f0baf..65384de7ed 100644 --- a/gitnexus/scripts/cross-platform-tests.ts +++ b/gitnexus/scripts/cross-platform-tests.ts @@ -170,6 +170,7 @@ const LBUG_NATIVE = [ 'test/integration/rename-provider-objective-c.test.ts', 'test/integration/rename-provider-php.test.ts', 'test/integration/rename-provider-python.test.ts', + 'test/integration/rename-provider-r.test.ts', 'test/integration/rename-provider-ruby.test.ts', 'test/integration/rename-provider-rust.test.ts', 'test/integration/rename-provider-swift.test.ts', diff --git a/gitnexus/src/core/ingestion/languages/r.ts b/gitnexus/src/core/ingestion/languages/r.ts index 015a3aad47..306f1a521d 100644 --- a/gitnexus/src/core/ingestion/languages/r.ts +++ b/gitnexus/src/core/ingestion/languages/r.ts @@ -20,6 +20,7 @@ import { rPostParse } from './r/post-parse.js'; import { rDefinitionProperties, rResolveMemberOwnerNode } from './r/owner-hooks.js'; import { isRNonNamingArgumentCapture } from './r/naming-argument.js'; import { createRCfgVisitor } from '../cfg/visitors/r.js'; +import { rRenameProvider } from './r/rename.js'; const R_BUILT_INS: ReadonlySet = new Set([ // Base R @@ -195,4 +196,5 @@ export const rProvider = defineLanguage({ shouldSkipDefinitionCapture: isRNonNamingArgumentCapture, // Control flow only (no def/use harvest): CFG + CDG, REACHING_DEF reports no-facts. cfgVisitor: createRCfgVisitor(), + rename: rRenameProvider, }); diff --git a/gitnexus/src/core/ingestion/languages/r/rename.ts b/gitnexus/src/core/ingestion/languages/r/rename.ts new file mode 100644 index 0000000000..bc771895f5 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/r/rename.ts @@ -0,0 +1,106 @@ +import type { NameClaim } from 'gitnexus-shared'; +import type { RenameIssue, RenameProvider } from '../../rename-provider.js'; +import { + createTreeSitterRenameSource, + innermostRenameScope, + walkNamedNodes, +} from '../rename-tree-sitter.js'; + +/** + * Conservative semantic rename for R. Plain functions are renamed through the + * scope-resolution graph. R binds names dynamically (S3/S4/R6 dispatch, strings, + * NAMESPACE), so every declaration or name that the graph cannot prove is refused. + */ +const RESERVED = new Set( + 'if else repeat while function for next break TRUE FALSE NULL Inf NaN NA NA_integer_ NA_real_ NA_character_ NA_complex_ in'.split( + ' ', + ), +); + +const unsupported = (message: string): RenameIssue => ({ + code: 'unsupported_occurrence', + status: 'unsupported', + message, +}); + +const parseNames = createTreeSitterRenameSource({ + isName: (node) => node.type === 'identifier', + skip: (node) => node.type === 'comment', + role: (node) => + node.parent?.type === 'parameter' && node.parent.childForFieldName('name')?.id === node.id + ? 'binding' + : 'name', +}); + +export const rRenameProvider: RenameProvider = { + validateName(name) { + if (!/^(?:[A-Za-z]|\.(?![0-9]))[A-Za-z0-9._]*$/.test(name) || RESERVED.has(name)) + return { code: 'invalid_name', message: 'Choose a valid nonreserved R identifier.' }; + }, + async parseSource(input) { + const source = await parseNames(input); + // Parameters shadow same-named functions: claim them per scope so the engine + // leaves an unrelated parameter alone. + const claims = new Map(); + for (const occurrence of source.occurrences) { + if (occurrence.role !== 'binding' || !occurrence.node) continue; + const scope = innermostRenameScope(source.parsed, occurrence.node); + if (!scope) continue; + claims.set(scope, [ + ...(claims.get(scope) ?? []), + { + name: occurrence.name, + range: occurrence.range, + kind: 'binding', + hoisted: true, + purpose: 'value', + }, + ]); + } + return { + ...source, + parsed: { + ...source.parsed, + scopes: source.parsed.scopes.map((scope) => ({ + ...scope, + nameClaims: [...(scope.nameClaims ?? []), ...(claims.get(scope.id) ?? [])], + })), + }, + }; + }, + checkSafety({ target, newName, files, graph }) { + const name = target.qualifiedName ?? ''; + if (target.type !== 'Function') + return unsupported('Only plain R functions can be renamed; classes and members are refused.'); + const defined = new Set( + [...files.values()].flatMap((source) => source.parsed.localDefs.map((d) => d.qualifiedName)), + ); + if (newName.includes('.') && defined.has(newName.slice(0, newName.indexOf('.')))) + return unsupported('The new name would register an S3 method of an existing function.'); + for (const source of files.values()) { + if (!source.tree) continue; + for (const node of walkNamedNodes(source.tree.rootNode)) { + if (node.type === 'string_content' && (node.text === name || node.text === newName)) + return unsupported( + 'A string literal spells this name; S3, S4, R6 and dynamic lookup resolve it by string.', + ); + } + } + // NAMESPACE is not an indexed source. A package function that the NAMESPACE + // exports (the graph keeps isExported unless it is proven private) is refused. + const file = target.filePath; + let exported = false; + graph.forEachNode((node) => { + if ( + node.properties.filePath === file && + node.properties.name === name && + node.properties.isExported !== false + ) + exported = true; + }); + if (exported && /(^|\/)R\//.test(file)) + return unsupported( + 'A package function may have a NAMESPACE export entry that is not renamed.', + ); + }, +}; diff --git a/gitnexus/test/fixtures/rename-integration/r/function-call-chain/after/chain.R b/gitnexus/test/fixtures/rename-integration/r/function-call-chain/after/chain.R new file mode 100644 index 0000000000..9290e97192 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/function-call-chain/after/chain.R @@ -0,0 +1,10 @@ +assist <- function(v) v + 1 + +step <- function(v) { + assist(assist(v)) +} + +# helper is described here +main <- function() { + step(1) +} diff --git a/gitnexus/test/fixtures/rename-integration/r/function-call-chain/before/chain.R b/gitnexus/test/fixtures/rename-integration/r/function-call-chain/before/chain.R new file mode 100644 index 0000000000..4c8d8a989d --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/function-call-chain/before/chain.R @@ -0,0 +1,10 @@ +helper <- function(v) v + 1 + +step <- function(v) { + helper(helper(v)) +} + +# helper is described here +main <- function() { + step(1) +} diff --git a/gitnexus/test/fixtures/rename-integration/r/function-call-chain/case.json b/gitnexus/test/fixtures/rename-integration/r/function-call-chain/case.json new file mode 100644 index 0000000000..1c8d96f435 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/function-call-chain/case.json @@ -0,0 +1,15 @@ +{ + "language": "r", + "description": "Rename a plain function called twice in one expression; a comment mentioning the name is untouched.", + "categories": ["call-graph", "same-file", "comments"], + "target": { "name": "helper", "kind": "Function", "file": "chain.R", "startLine": 0 }, + "newName": "assist", + "expect": { "status": "success", "edits": 3 }, + "edges": [ + { + "type": "CALLS", + "from": { "name": "step", "kind": "Function", "file": "chain.R", "startLine": 2 }, + "to": { "name": "helper", "kind": "Function", "file": "chain.R", "startLine": 0 } + } + ] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/after/apply.R b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/after/apply.R new file mode 100644 index 0000000000..0bcc3823aa --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/after/apply.R @@ -0,0 +1,3 @@ +apply_twice <- function(compute, v) { + compute(compute(v)) +} diff --git a/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/after/lib.R b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/after/lib.R new file mode 100644 index 0000000000..6d556fba7d --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/after/lib.R @@ -0,0 +1,7 @@ +calculate <- function(x) { + x * 2 +} + +run <- function(y) { + calculate(y) + 1 +} diff --git a/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/before/apply.R b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/before/apply.R new file mode 100644 index 0000000000..0bcc3823aa --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/before/apply.R @@ -0,0 +1,3 @@ +apply_twice <- function(compute, v) { + compute(compute(v)) +} diff --git a/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/before/lib.R b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/before/lib.R new file mode 100644 index 0000000000..a559079092 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/before/lib.R @@ -0,0 +1,7 @@ +compute <- function(x) { + x * 2 +} + +run <- function(y) { + compute(y) + 1 +} diff --git a/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/case.json b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/case.json new file mode 100644 index 0000000000..830c09b30a --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/function-unrelated-parameter/case.json @@ -0,0 +1,15 @@ +{ + "language": "r", + "description": "Rename a plain function called in its own file while a same-named parameter in a second file is left alone.", + "categories": ["call-graph", "cross-file", "parameter-shadowing"], + "target": { "name": "compute", "kind": "Function", "file": "lib.R", "startLine": 0 }, + "newName": "calculate", + "expect": { "status": "success", "edits": 2 }, + "edges": [ + { + "type": "CALLS", + "from": { "name": "run", "kind": "Function", "file": "lib.R", "startLine": 4 }, + "to": { "name": "compute", "kind": "Function", "file": "lib.R", "startLine": 0 } + } + ] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/after/lib.R b/gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/after/lib.R new file mode 100644 index 0000000000..a3d0cc6efd --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/after/lib.R @@ -0,0 +1,3 @@ +tick <- function(x) x + 1 + +run <- function() tick(1) diff --git a/gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/before/lib.R b/gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/before/lib.R new file mode 100644 index 0000000000..a3d0cc6efd --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/before/lib.R @@ -0,0 +1,3 @@ +tick <- function(x) x + 1 + +run <- function() tick(1) diff --git a/gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/case.json b/gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/case.json new file mode 100644 index 0000000000..47b42641f6 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/invalid-name-invalid-start/case.json @@ -0,0 +1,9 @@ +{ + "language": "r", + "description": "Refuse a name starting with a digit as the new function name.", + "categories": ["identifier-rules", "negative", "unchanged-on-error"], + "target": { "name": "tick", "kind": "Function", "file": "lib.R", "startLine": 0 }, + "newName": "2fast", + "expect": { "status": "error", "code": "invalid_name", "planningStatus": "blocked" }, + "edges": [] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/after/lib.R b/gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/after/lib.R new file mode 100644 index 0000000000..a3d0cc6efd --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/after/lib.R @@ -0,0 +1,3 @@ +tick <- function(x) x + 1 + +run <- function() tick(1) diff --git a/gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/before/lib.R b/gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/before/lib.R new file mode 100644 index 0000000000..a3d0cc6efd --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/before/lib.R @@ -0,0 +1,3 @@ +tick <- function(x) x + 1 + +run <- function() tick(1) diff --git a/gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/case.json b/gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/case.json new file mode 100644 index 0000000000..a83311268a --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/invalid-name-reserved-word/case.json @@ -0,0 +1,9 @@ +{ + "language": "r", + "description": "Refuse a reserved word as the new function name.", + "categories": ["identifier-rules", "negative", "unchanged-on-error"], + "target": { "name": "tick", "kind": "Function", "file": "lib.R", "startLine": 0 }, + "newName": "repeat", + "expect": { "status": "error", "code": "invalid_name", "planningStatus": "blocked" }, + "edges": [] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/namespace-export/after/DESCRIPTION b/gitnexus/test/fixtures/rename-integration/r/namespace-export/after/DESCRIPTION new file mode 100644 index 0000000000..2eb67eae12 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/namespace-export/after/DESCRIPTION @@ -0,0 +1,3 @@ +Package: pk +Version: 0.1.0 +Title: Fixture diff --git a/gitnexus/test/fixtures/rename-integration/r/namespace-export/after/NAMESPACE b/gitnexus/test/fixtures/rename-integration/r/namespace-export/after/NAMESPACE new file mode 100644 index 0000000000..7286717f2c --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/namespace-export/after/NAMESPACE @@ -0,0 +1 @@ +export(compute) diff --git a/gitnexus/test/fixtures/rename-integration/r/namespace-export/after/R/lib.R b/gitnexus/test/fixtures/rename-integration/r/namespace-export/after/R/lib.R new file mode 100644 index 0000000000..6a4cd1f3ff --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/namespace-export/after/R/lib.R @@ -0,0 +1,5 @@ +compute <- function(x) { + x * 2 +} + +use_it <- function() compute(2) diff --git a/gitnexus/test/fixtures/rename-integration/r/namespace-export/before/DESCRIPTION b/gitnexus/test/fixtures/rename-integration/r/namespace-export/before/DESCRIPTION new file mode 100644 index 0000000000..2eb67eae12 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/namespace-export/before/DESCRIPTION @@ -0,0 +1,3 @@ +Package: pk +Version: 0.1.0 +Title: Fixture diff --git a/gitnexus/test/fixtures/rename-integration/r/namespace-export/before/NAMESPACE b/gitnexus/test/fixtures/rename-integration/r/namespace-export/before/NAMESPACE new file mode 100644 index 0000000000..7286717f2c --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/namespace-export/before/NAMESPACE @@ -0,0 +1 @@ +export(compute) diff --git a/gitnexus/test/fixtures/rename-integration/r/namespace-export/before/R/lib.R b/gitnexus/test/fixtures/rename-integration/r/namespace-export/before/R/lib.R new file mode 100644 index 0000000000..6a4cd1f3ff --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/namespace-export/before/R/lib.R @@ -0,0 +1,5 @@ +compute <- function(x) { + x * 2 +} + +use_it <- function() compute(2) diff --git a/gitnexus/test/fixtures/rename-integration/r/namespace-export/case.json b/gitnexus/test/fixtures/rename-integration/r/namespace-export/case.json new file mode 100644 index 0000000000..8c8249e2c2 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/namespace-export/case.json @@ -0,0 +1,9 @@ +{ + "language": "r", + "description": "Refuse a package function that NAMESPACE exports, because the export entry would go stale.", + "categories": ["namespace", "package-export", "negative", "unchanged-on-error"], + "target": { "name": "compute", "kind": "Function", "file": "R/lib.R", "startLine": 0 }, + "newName": "calculate", + "expect": { "status": "error", "code": "unsupported_occurrence", "planningStatus": "unsupported" }, + "edges": [] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/r6-class/after/account.R b/gitnexus/test/fixtures/rename-integration/r/r6-class/after/account.R new file mode 100644 index 0000000000..528342f531 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/r6-class/after/account.R @@ -0,0 +1,11 @@ +Account <- R6::R6Class("Account", + public = list( + balance = 0, + deposit = function(n) { + self$balance <- self$balance + n + invisible(self) + } + ) +) + +acc <- Account$new() diff --git a/gitnexus/test/fixtures/rename-integration/r/r6-class/before/account.R b/gitnexus/test/fixtures/rename-integration/r/r6-class/before/account.R new file mode 100644 index 0000000000..528342f531 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/r6-class/before/account.R @@ -0,0 +1,11 @@ +Account <- R6::R6Class("Account", + public = list( + balance = 0, + deposit = function(n) { + self$balance <- self$balance + n + invisible(self) + } + ) +) + +acc <- Account$new() diff --git a/gitnexus/test/fixtures/rename-integration/r/r6-class/case.json b/gitnexus/test/fixtures/rename-integration/r/r6-class/case.json new file mode 100644 index 0000000000..22534ac9c5 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/r6-class/case.json @@ -0,0 +1,9 @@ +{ + "language": "r", + "description": "Refuse an R6 class whose members and constructor are reached through $new and self.", + "categories": ["r6", "class", "negative", "unchanged-on-error"], + "target": { "name": "Account", "kind": "Class", "file": "account.R", "startLine": 0 }, + "newName": "Wallet", + "expect": { "status": "error", "code": "unsupported_occurrence", "planningStatus": "unsupported" }, + "edges": [] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/s3-generic/after/shapes.R b/gitnexus/test/fixtures/rename-integration/r/s3-generic/after/shapes.R new file mode 100644 index 0000000000..8890abbe3d --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s3-generic/after/shapes.R @@ -0,0 +1,5 @@ +area <- function(x) UseMethod("area") + +area.circle <- function(x) 3.14 * x$r^2 + +total <- function(s) area(s) diff --git a/gitnexus/test/fixtures/rename-integration/r/s3-generic/before/shapes.R b/gitnexus/test/fixtures/rename-integration/r/s3-generic/before/shapes.R new file mode 100644 index 0000000000..8890abbe3d --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s3-generic/before/shapes.R @@ -0,0 +1,5 @@ +area <- function(x) UseMethod("area") + +area.circle <- function(x) 3.14 * x$r^2 + +total <- function(s) area(s) diff --git a/gitnexus/test/fixtures/rename-integration/r/s3-generic/case.json b/gitnexus/test/fixtures/rename-integration/r/s3-generic/case.json new file mode 100644 index 0000000000..c0636332f0 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s3-generic/case.json @@ -0,0 +1,9 @@ +{ + "language": "r", + "description": "Refuse an S3 generic whose UseMethod string and area.circle method dispatch by spelling.", + "categories": ["s3", "string-dispatch", "negative", "unchanged-on-error"], + "target": { "name": "area", "kind": "Function", "file": "shapes.R", "startLine": 0 }, + "newName": "surface", + "expect": { "status": "error", "code": "unsupported_occurrence", "planningStatus": "unsupported" }, + "edges": [] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/s3-method/after/shapes.R b/gitnexus/test/fixtures/rename-integration/r/s3-method/after/shapes.R new file mode 100644 index 0000000000..2958e04734 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s3-method/after/shapes.R @@ -0,0 +1,3 @@ +area <- function(x) UseMethod("area") + +area.circle <- function(x) 3.14 * x$r^2 diff --git a/gitnexus/test/fixtures/rename-integration/r/s3-method/before/shapes.R b/gitnexus/test/fixtures/rename-integration/r/s3-method/before/shapes.R new file mode 100644 index 0000000000..2958e04734 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s3-method/before/shapes.R @@ -0,0 +1,3 @@ +area <- function(x) UseMethod("area") + +area.circle <- function(x) 3.14 * x$r^2 diff --git a/gitnexus/test/fixtures/rename-integration/r/s3-method/case.json b/gitnexus/test/fixtures/rename-integration/r/s3-method/case.json new file mode 100644 index 0000000000..59dc8952d2 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s3-method/case.json @@ -0,0 +1,23 @@ +{ + "language": "r", + "description": "Refuse an S3 method: its dotted name is read as an owner-qualified member, so the graph identity is not a unique source declaration.", + "categories": [ + "s3", + "dotted-name", + "negative", + "unchanged-on-error" + ], + "target": { + "name": "area.circle", + "kind": "Function", + "file": "shapes.R", + "startLine": 2 + }, + "newName": "round_area", + "expect": { + "status": "error", + "code": "stale_graph", + "planningStatus": "blocked" + }, + "edges": [] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/after/shapes.R b/gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/after/shapes.R new file mode 100644 index 0000000000..0732dc9079 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/after/shapes.R @@ -0,0 +1,3 @@ +area <- function(x) 1 + +circle_area <- function(x) 3.14 * x$r^2 diff --git a/gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/before/shapes.R b/gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/before/shapes.R new file mode 100644 index 0000000000..0732dc9079 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/before/shapes.R @@ -0,0 +1,3 @@ +area <- function(x) 1 + +circle_area <- function(x) 3.14 * x$r^2 diff --git a/gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/case.json b/gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/case.json new file mode 100644 index 0000000000..a6de441003 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s3-new-method-name/case.json @@ -0,0 +1,9 @@ +{ + "language": "r", + "description": "Refuse a new dotted name whose prefix is an existing function, which would silently register an S3 method of it.", + "categories": ["s3", "name-collision", "negative", "unchanged-on-error"], + "target": { "name": "circle_area", "kind": "Function", "file": "shapes.R", "startLine": 2 }, + "newName": "area.circle", + "expect": { "status": "error", "code": "unsupported_occurrence", "planningStatus": "unsupported" }, + "edges": [] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/s4-class/after/person.R b/gitnexus/test/fixtures/rename-integration/r/s4-class/after/person.R new file mode 100644 index 0000000000..105422c610 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s4-class/after/person.R @@ -0,0 +1,3 @@ +setClass("Person", representation(name = "character")) + +make <- function() new("Person", name = "a") diff --git a/gitnexus/test/fixtures/rename-integration/r/s4-class/before/person.R b/gitnexus/test/fixtures/rename-integration/r/s4-class/before/person.R new file mode 100644 index 0000000000..105422c610 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s4-class/before/person.R @@ -0,0 +1,3 @@ +setClass("Person", representation(name = "character")) + +make <- function() new("Person", name = "a") diff --git a/gitnexus/test/fixtures/rename-integration/r/s4-class/case.json b/gitnexus/test/fixtures/rename-integration/r/s4-class/case.json new file mode 100644 index 0000000000..ed0f7d4f74 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/s4-class/case.json @@ -0,0 +1,9 @@ +{ + "language": "r", + "description": "Refuse an S4 class whose name is spelled only in string literals.", + "categories": ["s4", "class", "negative", "unchanged-on-error"], + "target": { "name": "Person", "kind": "Class", "file": "person.R", "startLine": 0 }, + "newName": "Human", + "expect": { "status": "error", "code": "unsupported_occurrence", "planningStatus": "unsupported" }, + "edges": [] +} diff --git a/gitnexus/test/fixtures/rename-integration/r/string-lookup/after/dyn.R b/gitnexus/test/fixtures/rename-integration/r/string-lookup/after/dyn.R new file mode 100644 index 0000000000..0e5d5d0299 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/string-lookup/after/dyn.R @@ -0,0 +1,3 @@ +worker <- function(x) x + 1 + +run <- function() do.call("worker", list(1)) diff --git a/gitnexus/test/fixtures/rename-integration/r/string-lookup/before/dyn.R b/gitnexus/test/fixtures/rename-integration/r/string-lookup/before/dyn.R new file mode 100644 index 0000000000..0e5d5d0299 --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/string-lookup/before/dyn.R @@ -0,0 +1,3 @@ +worker <- function(x) x + 1 + +run <- function() do.call("worker", list(1)) diff --git a/gitnexus/test/fixtures/rename-integration/r/string-lookup/case.json b/gitnexus/test/fixtures/rename-integration/r/string-lookup/case.json new file mode 100644 index 0000000000..a27d82360b --- /dev/null +++ b/gitnexus/test/fixtures/rename-integration/r/string-lookup/case.json @@ -0,0 +1,9 @@ +{ + "language": "r", + "description": "Refuse a function that a string literal names for do.call dynamic lookup.", + "categories": ["string-dispatch", "negative", "unchanged-on-error"], + "target": { "name": "worker", "kind": "Function", "file": "dyn.R", "startLine": 0 }, + "newName": "job", + "expect": { "status": "error", "code": "unsupported_occurrence", "planningStatus": "unsupported" }, + "edges": [] +} diff --git a/gitnexus/test/integration/rename-provider-r.test.ts b/gitnexus/test/integration/rename-provider-r.test.ts new file mode 100644 index 0000000000..637562d100 --- /dev/null +++ b/gitnexus/test/integration/rename-provider-r.test.ts @@ -0,0 +1,3 @@ +import { defineRenameFixtureTests } from '../helpers/rename-integration-cases.js'; + +defineRenameFixtureTests('r');