diff --git a/gitnexus/src/core/ingestion/languages/r/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/r/scope-resolver.ts index 98175fe1a..48c389e5d 100644 --- a/gitnexus/src/core/ingestion/languages/r/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/r/scope-resolver.ts @@ -104,7 +104,8 @@ function emitRVirtualHeritageEdges( } /** - * Enumerate all top-level names exported from a target module scope's file. + * Enumerate all top-level names exported from a target module scope's file + * (module-scope function and class bindings; functions nested in another function are not). * `library()`/`require()` are R's wildcard imports — every top-level name of * the named local package becomes visible, UNFILTERED by NAMESPACE (a * `library()`/`require()` call brings the whole package's namespace into @@ -127,12 +128,15 @@ export function expandRWildcardNames( ): readonly string[] { const target = parsedFiles.find((p) => p.moduleScope === targetModuleScope); if (target === undefined) return []; - const { dottedTails } = rFileTopLevel(target); + const { names: topLevel, dottedTails } = rFileTopLevel(target); const seen = new Set(); const names: string[] = []; for (const def of target.localDefs) { const qn = def.qualifiedName; - if (qn === undefined || qn.length === 0 || qn.includes('.')) continue; // top-level only + if (qn === undefined || qn.length === 0 || qn.includes('.')) continue; // no dotted names + // `localDefs` is flat: it also lists functions nested in other functions, which a + // `library()` does not bring into scope. Only module-scope bindings are top-level. + if (!topLevel.has(qn)) continue; if (dottedTails.has(qn)) continue; // shares its tail with an S3 method: see above if (seen.has(qn)) continue; seen.add(qn); diff --git a/gitnexus/test/unit/scope-resolution/r/r-dotted-s3-hooks.test.ts b/gitnexus/test/unit/scope-resolution/r/r-dotted-s3-hooks.test.ts index b860bd137..3df1da79d 100644 --- a/gitnexus/test/unit/scope-resolution/r/r-dotted-s3-hooks.test.ts +++ b/gitnexus/test/unit/scope-resolution/r/r-dotted-s3-hooks.test.ts @@ -104,6 +104,39 @@ describe('expandRWildcardNames', () => { ).toEqual(['Scorer', 'tidy']); }); + it('lists only top-level definitions, not functions nested in another function', () => { + expect( + names('outer <- function() {\n helper <- function() 1\n helper()\n}\ntop <- function() 2'), + ).toEqual(['outer', 'top']); + }); + + it('does not list a function nested several levels deep or inside a control block', () => { + expect( + names( + [ + 'a <- function() {', + ' b <- function() {', + ' c <- function() 1', + ' }', + ' if (TRUE) d <- function() 2', + '}', + ].join('\n'), + ), + ).toEqual(['a']); + }); + + it('still lists top-level classes alongside top-level functions', () => { + expect( + names('Scorer <- R6::R6Class("Scorer", public = list(f = function() 1))\nrun = function() 1'), + ).toEqual(['Scorer', 'run']); + }); + + it('still lists an S4 class defined by a bare setClass() call', () => { + expect( + names('setClass("Shape", representation(n = "numeric"))\nplain <- function() 1'), + ).toEqual(['Shape', 'plain']); + }); + it('never lists dotted names themselves', () => { expect(names('print.foo <- function(x) x')).toEqual([]); });