diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3ba8bc..f9421824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- In R, a variable assigned at the top of a test file is no longer linked from the package or from other tests, because testthat runs each test file in its own environment. On ggplot2, a test's `c <- ggplot(…)` used to collect 2,455 of the package's `c(…)` calls. Variables in `helper-*.R` and `setup-*.R`, which testthat loads for every test, are still shared. - In Lua and Luau, a `local` variable or function is no longer linked from other files, since it's only visible in the file that declares it. On kong, a spec helper's `local it = it` used to collect every other spec file's `it(…)` (over 4,000 links), and one module's `local ipairs = ipairs` took the whole project's `ipairs(…)`. Global functions are still linked across files. - PHP calls on `$this`, `self`, `static` and `parent` now reach the method of the class they're written in, a class it extends, or a trait any of them uses, with parent classes and traits found through the file's `namespace` and `use` imports. On Drupal core, `$this->assertEquals()` (PHPUnit's) used to link to an unrelated comparator class 8,832 times, `$this->assertSession()` to a JavaScript-test base class, and `$this->t()` to `Views::t`. They now reach `UiHelperTrait` and `StringTranslationTrait`, or nothing when the method lives in a package outside the repository. A call inside a trait, and a base class calling a method its subclass defines, keep their links. Re-index PHP projects after upgrading. - In PHP, CFML, Pascal, COBOL and VB.NET, a call written in a different case than its definition now resolves, like `formatprice()` for `FormatPrice` or CFML's `ToMatchWithCase()` for `toMatchWithCase`. These languages ignore case in names, but such calls used to be dropped before resolution. diff --git a/__tests__/r-test-file-scope.test.ts b/__tests__/r-test-file-scope.test.ts new file mode 100644 index 00000000..7db573b7 --- /dev/null +++ b/__tests__/r-test-file-scope.test.ts @@ -0,0 +1,57 @@ +/** + * testthat runs each test file in an environment of its own: a variable one + * test assigns is invisible to the package and to other tests. ggplot2's + * `c <- ggplot(…)` in one test took the package's 2,455 `c(…)` calls. A + * `helper-*.R` file is sourced for every test, so its bindings stay shared, + * and a package function defined through a factory + * (`geom_point <- make_constructor(…)`) stays callable. + */ +import { describe, it, expect, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +const roots: string[] = []; +afterAll(() => { + for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true }); +}); + +describe('R test-file bindings', () => { + it('stay in their file, except helpers; package bindings stay callable', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-r-tests-')); + roots.push(root); + const files: Record = { + DESCRIPTION: 'Package: demo\n', + 'R/geom-point.R': `geom_point <- make_constructor(GeomPoint) +table_of <- function() { + c("a", "b") +} +`, + 'tests/testthat/helper-s3.R': `quux <- structure(list(), class = "quux") +`, + 'tests/testthat/test-scales.R': `c <- ggplot(df, aes(x, y)) +test_that("works", { + geom_point() + quux() +}) +`, + }; + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + const cg = await CodeGraph.init(root, { index: true }); + try { + const targets = (file: string) => + cg + .getOutgoingEdgesFrom(cg.getNodesInFile(file).map((n) => n.id), ['calls']) + .map((e) => `${cg.getNode(e.target)!.name}@${cg.getNode(e.target)!.filePath}`) + .sort(); + expect(targets('R/geom-point.R')).toEqual([]); + expect(targets('tests/testthat/test-scales.R')).toEqual(['geom_point@R/geom-point.R', 'quux@tests/testthat/helper-s3.R']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 683a3aa7..5e587727 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -944,6 +944,11 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex return ref.filePath.startsWith(owner + '/'); } if (PRIVATE_IS_FILE_LOCAL.has(lang)) return candidate.visibility !== 'private'; + // An R test file runs in an environment of its own (testthat): its top-level + // `c <- ggplot(…)` is not what the package's 2,843 `c(…)` calls mean. The + // `helper-*.R` / `setup-*.R` files are sourced for every test, so theirs are shared. + if (lang === 'r' && (candidate.kind === 'variable' || candidate.kind === 'constant') && + /(?:^|\/)tests?\//.test(candidate.filePath) && !/(?:^|\/)(?:helper|setup)[^/]*\.[rR]$/.test(candidate.filePath)) return false; // A Lua `local` belongs to its chunk: kong's spec helpers' `local it = it` // took busted's `it(…)` in every other spec file, 4,166 times. if ((lang === 'lua' || lang === 'luau') && isLuaLocal(candidate, context)) return false;