From 06ae82c4e3e723c65cc653d848072a7edc370ca5 Mon Sep 17 00:00:00 2001 From: Mick Cooney Date: Sat, 17 Oct 2026 14:55:58 +0100 Subject: [PATCH] docs(r): say that NAMESPACE refines graph nodes, not scope definitions refineRExportStatus flips isExported on graph nodes only. The scope definitions that finalization reads carry no export verdict for R (undefined means unknown), which is deliberate: library() attaches every top-level name, as devtools::load_all() does. State that in the provider guide and pin it with a test, so a later change that stamps a verdict onto scope definitions is a conscious one. --- docs/languages/r-provider.md | 2 +- .../r/r-export-evidence.test.ts | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 gitnexus/test/unit/scope-resolution/r/r-export-evidence.test.ts diff --git a/docs/languages/r-provider.md b/docs/languages/r-provider.md index 920a55d20..abee5babc 100644 --- a/docs/languages/r-provider.md +++ b/docs/languages/r-provider.md @@ -58,7 +58,7 @@ The scope query (`languages/r/query.ts`) captures three call shapes only: `f(x)` - Discovery may have missed a package (its limits were reached): the call keeps the name-based behaviour. - `::` and `:::` are not distinguished, so `pkg::name` binds to a definition whether or not the package exports it. - Package discovery is bounded to depth 3 and 200 directories (when the bound may have hidden a package this is flagged internally and nothing is logged), reads directories in code-unit order, ranks duplicate package names by shallowest then by whole path, and never searches directories named exactly `renv`, `packrat`, `revdep` or `.Rcheck`. The shared file scanner has no such ignore entries, so `.R` files inside those trees are still parsed even though discovery skips them. -- Export status: the language-level export check treats every symbol as public. The NAMESPACE pass then marks the remaining `R/` symbols of a NAMESPACE-bearing package non-exported. Symbols outside `R/` (`tests/`, `vignettes/`, `inst/`) are not package members and stay public; files in `R/` subdirectories are treated as `R/` files although R itself does not load them. A package without a NAMESPACE stays fully public, and a nested package without one does not inherit its enclosing package's. roxygen2 `@export` tags are not consulted. +- Export status: the language-level export check treats every symbol as public. The NAMESPACE pass then marks the remaining `R/` symbols of a NAMESPACE-bearing package non-exported. Symbols outside `R/` (`tests/`, `vignettes/`, `inst/`) are not package members and stay public; files in `R/` subdirectories are treated as `R/` files although R itself does not load them. A package without a NAMESPACE stays fully public, and a nested package without one does not inherit its enclosing package's. roxygen2 `@export` tags are not consulted. The refinement changes the graph nodes' `isExported` only; the definitions scope resolution works from carry no export verdict, so `library()`, `pkg::name` and the global-name fallback do not consult NAMESPACE (only `importFrom()` does). - `exportPattern()` text comes from the analysed repository, so patterns are compiled to a linear-time automaton (`linear-regex.ts`) with a work budget. Constructs with no linear-time algorithm (back-references, look-around) and oversized patterns are dropped with one logged warning per pattern per package. Character classes use ASCII semantics. A directive argument the NAMESPACE reader cannot read at all is dropped without a warning: an R 4.0 raw string (`exportPattern(r"(^[^.])")`, `export(r"(foo)")`) and any string that spans lines. It exports nothing, so the names it was meant to cover read as non-exported. ## Limits and known gaps diff --git a/gitnexus/test/unit/scope-resolution/r/r-export-evidence.test.ts b/gitnexus/test/unit/scope-resolution/r/r-export-evidence.test.ts new file mode 100644 index 000000000..e709bc230 --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/r/r-export-evidence.test.ts @@ -0,0 +1,40 @@ +/** + * R gives scope resolution no export verdict. `SymbolDefinition.isExported` is tri-state and + * `undefined` means "unknown" (finalization then keeps its prior behaviour); R's export checker + * says "public" for every graph node and the NAMESPACE refinement later flips graph nodes to + * private, but neither writes a verdict onto a scope definition. That is deliberate: a + * `library()` of a local package makes every top-level name visible, as `devtools::load_all()` + * does (docs/languages/r-provider.md, Resolution model), so resolution must not treat a name + * NAMESPACE omits as module-private. A verdict of `false` would make finalization drop it from + * wildcard providers. + */ +import { describe, expect, it } from 'vitest'; +import { extractParsedFile } from '../../../../src/core/ingestion/scope-extractor-bridge.js'; +import { rProvider } from '../../../../src/core/ingestion/languages/r.js'; + +describe('R scope definitions carry no export verdict', () => { + const parsed = extractParsedFile( + rProvider, + [ + 'exported_fn <- function() 1', + 'Widget <- R6Class("Widget", public = list(size = 1, grow = function() 1))', + 'setClass("Point", representation(x = "numeric"))', + 'setGeneric("area", function(shape) standardGeneric("area"))', + ].join('\n'), + 'R/a.R', + ); + + it('extracts functions, classes and members (positive control)', () => { + const types = new Set(parsed?.localDefs.map((d) => d.type)); + expect(types.has('Function')).toBe(true); + expect(types.has('Class')).toBe(true); + expect(types.has('Method')).toBe(true); + }); + + it('leaves isExported unset on every definition', () => { + expect(parsed?.localDefs.length).toBeGreaterThan(0); + for (const def of parsed?.localDefs ?? []) { + expect(def.isExported, `${def.type} ${def.qualifiedName}`).toBeUndefined(); + } + }); +});