mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-15 04:18:05 +00:00
feat(r): wire R onto the scope-resolution pipeline
Add emitRScopeCaptures/interpretRImport/interpretRTypeBinding and a new
rScopeResolver, modeled on Zig's/Ruby's shape, and register it in
SCOPE_RESOLVERS. Captures cover scopes (function/R6-class/S4-class, the
class scope spanning the whole R6Class()/setClass() call so inherit=/
contains= sites resolve inside it), declarations (function, class without
a qualified_name so the bare-name fallback matches the legacy node,
R6/R5 methods), imports (source() as side-effect, library()/require() as
wildcard per the unfiltered-by-NAMESPACE policy), the R6 constructor
type-binding (Type$new(...)), and heritage references (R6 inherit=, S4
contains=/contains=c()), which resolve via the generic
preEmitInheritanceEdges mechanism directly.
38/39 r.test.ts scenarios pass; only the VIRTUAL sentinel (no
setClass("VIRTUAL", ...) exists anywhere) remains, requiring a custom
emitHeritageEdges hook.
Adds R's entries to the two registry-census unit tests
(import-target-index-reuse.contract.test.ts,
external-import-conformance.test.ts) that assert every SCOPE_RESOLVERS
key has a fixture/case.
This commit is contained in:
parent
b9a4ed619f
commit
658e5ce840
9 changed files with 555 additions and 0 deletions
|
|
@ -14,6 +14,8 @@ import { resolveRImport } from '../import-resolvers/r.js';
|
|||
import { R_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { RFieldExtractor } from '../field-extractors/r.js';
|
||||
import { rMethodExtractor } from '../method-extractors/r.js';
|
||||
import { emitRScopeCaptures } from './r/captures.js';
|
||||
import { interpretRImport, interpretRTypeBinding } from './r/interpret.js';
|
||||
|
||||
const R_BUILT_INS: ReadonlySet<string> = new Set([
|
||||
// Base R
|
||||
|
|
@ -178,4 +180,7 @@ export const rProvider = defineLanguage({
|
|||
fieldExtractor: new RFieldExtractor(),
|
||||
methodExtractor: rMethodExtractor,
|
||||
builtInNames: R_BUILT_INS,
|
||||
emitScopeCaptures: emitRScopeCaptures,
|
||||
interpretImport: interpretRImport,
|
||||
interpretTypeBinding: interpretRTypeBinding,
|
||||
});
|
||||
|
|
|
|||
44
gitnexus/src/core/ingestion/languages/r/captures.ts
Normal file
44
gitnexus/src/core/ingestion/languages/r/captures.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { Capture, CaptureMatch } from 'gitnexus-shared';
|
||||
import { nodeToCapture } from '../../utils/ast-helpers.js';
|
||||
import { getRParser, getRScopeQuery } from './query.js';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
|
||||
/**
|
||||
* Emit R scope captures — a single tree-sitter query run against the R
|
||||
* grammar (see `query.ts` for the capture shapes and the rationale for each
|
||||
* one). No programmatic enrichment is needed: unlike C/Zig/Rust, R's
|
||||
* declarations carry no synthesized arity/parameter metadata, and heritage
|
||||
* sites resolve directly by name (no marker encoding, unlike Ruby's
|
||||
* `include`/`extend`/`prepend`).
|
||||
*/
|
||||
export function emitRScopeCaptures(
|
||||
sourceText: string,
|
||||
filePath: string,
|
||||
cachedTree?: unknown,
|
||||
): readonly CaptureMatch[] {
|
||||
let tree = cachedTree as ReturnType<ReturnType<typeof getRParser>['parse']> | undefined;
|
||||
if (tree === undefined) {
|
||||
tree = parseSourceSafe(getRParser(), sourceText, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(sourceText),
|
||||
});
|
||||
}
|
||||
|
||||
const rawMatches = getRScopeQuery().matches(tree.rootNode);
|
||||
const out: CaptureMatch[] = [];
|
||||
|
||||
for (const m of rawMatches) {
|
||||
const grouped: Record<string, Capture> = {};
|
||||
let hasRealCapture = false;
|
||||
for (const c of m.captures) {
|
||||
const tag = '@' + c.name;
|
||||
if (tag.startsWith('@_')) continue;
|
||||
grouped[tag] = nodeToCapture(tag, c.node);
|
||||
hasRealCapture = true;
|
||||
}
|
||||
if (!hasRealCapture) continue;
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
40
gitnexus/src/core/ingestion/languages/r/interpret.ts
Normal file
40
gitnexus/src/core/ingestion/languages/r/interpret.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { CaptureMatch, ParsedImport, ParsedTypeBinding } from 'gitnexus-shared';
|
||||
|
||||
/**
|
||||
* `source()` is a side-effect import — it establishes a file dependency but
|
||||
* binds no name (cross-file free calls into a `source()`d file resolve via
|
||||
* `rScopeResolver.allowGlobalFreeCallFallback` instead).
|
||||
*
|
||||
* `library()`/`require()` are wildcard imports — every top-level name of the
|
||||
* named local package becomes visible (`rProvider.expandsWildcardTo`).
|
||||
*
|
||||
* `resolveRImportTarget` (`import-resolvers/r.ts`) guards on
|
||||
* `context.parsedImport.kind === 'wildcard'` specifically to refuse a
|
||||
* `library()`/`require()` package name that isn't a known local package
|
||||
* (rather than falling through to a cross-language suffix-match false
|
||||
* positive) — the two kinds set here are what that guard keys on.
|
||||
*/
|
||||
export function interpretRImport(captures: CaptureMatch): ParsedImport | null {
|
||||
const source = captures['@import.source']?.text;
|
||||
if (source === undefined || source.length === 0) return null;
|
||||
|
||||
if (captures['@import.wildcard'] !== undefined) {
|
||||
return { kind: 'wildcard', targetRaw: source };
|
||||
}
|
||||
return { kind: 'side-effect', targetRaw: source };
|
||||
}
|
||||
|
||||
/**
|
||||
* `x <- Type$new(...)` — R6's constructor spelling. `@type-binding.type` is
|
||||
* the `$` receiver (the class name), captured only when the extract
|
||||
* operator's rhs is literally `new` (see `query.ts`), so this always
|
||||
* resolves via the shared `constructor-inferred` sub-kind — the same one
|
||||
* Zig's `interpretZigTypeBinding` uses for `Counter.init()`.
|
||||
*/
|
||||
export function interpretRTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null {
|
||||
const name = captures['@type-binding.name']?.text;
|
||||
const type = captures['@type-binding.type']?.text;
|
||||
if (name === undefined || type === undefined) return null;
|
||||
|
||||
return { boundName: name, rawTypeName: type, source: 'constructor-inferred' };
|
||||
}
|
||||
301
gitnexus/src/core/ingestion/languages/r/query.ts
Normal file
301
gitnexus/src/core/ingestion/languages/r/query.ts
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* Tree-sitter query for R scope captures (RFC #909 Ring 3, kaybenleroll/GitNexus#1).
|
||||
*
|
||||
* R has no dedicated class syntax — R6 classes are `R6Class(...)` calls, S4/R5
|
||||
* classes are `setClass(...)`/`setRefClass(...)` calls — so this query mirrors
|
||||
* the shapes the legacy `R_QUERIES` (`tree-sitter-queries.ts`) already proved
|
||||
* queryable for structural extraction, renamed onto the scope-resolution
|
||||
* capture vocabulary (`@scope.*`, `@declaration.*`, `@reference.*`,
|
||||
* `@import.*`, `@type-binding.*`).
|
||||
*
|
||||
* R specifics encoded here:
|
||||
*
|
||||
* - A Class scope spans the ENTIRE `R6Class(...)`/`setClass(...)` call
|
||||
* (the whole `binary_operator` for the `name <- R6Class(...)` shape),
|
||||
* not just `public = list(...)` — the `inherit=`/`contains=` argument
|
||||
* must fall inside the scope's range so the `@reference.inherits` sites
|
||||
* resolve against the deriving class's own scope (§6/stress-test).
|
||||
*
|
||||
* - `@declaration.function`/`@declaration.class` anchor on the WHOLE
|
||||
* `name <- <rhs>` assignment (matching the legacy `@definition.*`
|
||||
* anchors exactly), so `resolveDefGraphId`'s position-based (Function)
|
||||
* and qualifiedName-keyed (Class) reconciliation lands on the SAME
|
||||
* graph node the legacy extractor already created.
|
||||
*
|
||||
* - `@declaration.method` anchors on the `name = function(...)` ARGUMENT
|
||||
* inside `public =`/`private =`/`active =`/`methods = list(...)` — a
|
||||
* narrower range than the enclosing Class scope, so Pass 2 attaches the
|
||||
* method as an owned def of the class scope directly (no hoist).
|
||||
*
|
||||
* - No `@declaration.qualified_name` is emitted for `@declaration.class`
|
||||
* — R's legacy extractor has no `classExtractor`, so a Class node's
|
||||
* `qualifiedName` today is the bare class-name text. Omitting the
|
||||
* capture lets `buildDefFromDeclarationMatch` default `qualifiedName`
|
||||
* to the bare name, matching the legacy node so `resolveDefGraphId`'s
|
||||
* name-keyed fallback finds it.
|
||||
*
|
||||
* - S4/setGeneric/setMethod declarations are deliberately NOT captured
|
||||
* here — they are already produced by the legacy extractor via
|
||||
* R_QUERIES, independent of scope-resolution, and none of the target
|
||||
* scenarios needs a NEW declaration for them. Their inner
|
||||
* `function_definition` bodies still get a `@scope.function` from the
|
||||
* blanket rule below (an unnamed scope, harmless).
|
||||
*
|
||||
* - `library()`/`require()` are wildcard imports (`@import.wildcard`) —
|
||||
* every top-level name of the named local package becomes visible,
|
||||
* unfiltered by NAMESPACE (see `rProvider.expandsWildcardTo`).
|
||||
* `source()` is a side-effect import (`@import.side-effect`) — it
|
||||
* establishes a file dependency but binds no name; cross-file free
|
||||
* calls into a `source()`d file resolve via `allowGlobalFreeCallFallback`
|
||||
* instead (`rScopeResolver`).
|
||||
*
|
||||
* - `obj$method()` (the `extract_operator`, R's `$`) is a member call with
|
||||
* an explicit receiver — the same shape as Ruby's `obj.method`.
|
||||
* `Type$new(...)` is R6's constructor spelling: captured separately as
|
||||
* `@type-binding.constructor` so `x <- Type$new(...)` types `x`.
|
||||
*
|
||||
* Exposes lazy `Parser` and `Query` singletons so callers don't pay
|
||||
* tree-sitter init cost per file.
|
||||
*/
|
||||
|
||||
import Parser from 'tree-sitter';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
import R from '@eagleoutice/tree-sitter-r';
|
||||
|
||||
/**
|
||||
* Exported for `value-ref-dispatchability.test.ts`-style whole-query readers.
|
||||
* Not part of the provider surface — nothing else should import it.
|
||||
*/
|
||||
export const R_SCOPE_QUERY = `
|
||||
;; ── Scopes ───────────────────────────────────────────────────────────────
|
||||
|
||||
(program) @scope.module
|
||||
|
||||
(function_definition) @scope.function
|
||||
|
||||
;; R6 class via namespace call (ClassName <- R6::R6Class(...)) — the scope
|
||||
;; spans the WHOLE assignment, including inherit=, so heritage sites resolve
|
||||
;; inside the deriving class's own scope.
|
||||
(binary_operator
|
||||
lhs: (identifier)
|
||||
rhs: (call
|
||||
function: (namespace_operator
|
||||
rhs: (identifier) @_r6ns_scope
|
||||
(#match? @_r6ns_scope "^R6Class$")))) @scope.class
|
||||
|
||||
;; R6 class via bare call (ClassName <- R6Class(...))
|
||||
(binary_operator
|
||||
lhs: (identifier)
|
||||
rhs: (call
|
||||
function: (identifier) @_r6bare_scope
|
||||
(#match? @_r6bare_scope "^R6Class$"))) @scope.class
|
||||
|
||||
;; S4/R5 class (setClass(...) / setRefClass(...)) — scope spans the whole call.
|
||||
(call
|
||||
function: (identifier) @_s4_scope
|
||||
(#match? @_s4_scope "^(setClass|setRefClass)$")) @scope.class
|
||||
|
||||
;; ── Declarations — functions ──────────────────────────────────────────────
|
||||
|
||||
(binary_operator
|
||||
lhs: (identifier) @declaration.name
|
||||
rhs: (function_definition)) @declaration.function
|
||||
|
||||
;; ── Declarations — classes (name only; no qualified_name, see file header) ─
|
||||
|
||||
(binary_operator
|
||||
lhs: (identifier) @declaration.name
|
||||
rhs: (call
|
||||
function: (namespace_operator
|
||||
rhs: (identifier) @_r6ns_decl
|
||||
(#match? @_r6ns_decl "^R6Class$")))) @declaration.class
|
||||
|
||||
(binary_operator
|
||||
lhs: (identifier) @declaration.name
|
||||
rhs: (call
|
||||
function: (identifier) @_r6bare_decl
|
||||
(#match? @_r6bare_decl "^R6Class$"))) @declaration.class
|
||||
|
||||
(call
|
||||
function: (identifier) @_s4_decl
|
||||
(#match? @_s4_decl "^(setClass|setRefClass)$")
|
||||
arguments: (arguments
|
||||
. (argument
|
||||
value: (string
|
||||
content: (string_content) @declaration.name)))) @declaration.class
|
||||
|
||||
;; ── Declarations — R6/R5 methods ──────────────────────────────────────────
|
||||
|
||||
;; R6 methods via namespace call (inside public/private/active = list(...))
|
||||
(binary_operator
|
||||
lhs: (identifier) @_class1
|
||||
rhs: (call
|
||||
function: (namespace_operator
|
||||
rhs: (identifier) @_r6ns_m
|
||||
(#match? @_r6ns_m "^R6Class$"))
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @_section1
|
||||
(#match? @_section1 "^(public|private|active)$")
|
||||
value: (call
|
||||
function: (identifier) @_listfn1
|
||||
(#match? @_listfn1 "^list$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @declaration.name
|
||||
value: (function_definition)) @declaration.method))))))
|
||||
|
||||
;; R6 methods via bare call (inside public/private/active = list(...))
|
||||
(binary_operator
|
||||
lhs: (identifier) @_class2
|
||||
rhs: (call
|
||||
function: (identifier) @_r6bare_m
|
||||
(#match? @_r6bare_m "^R6Class$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @_section2
|
||||
(#match? @_section2 "^(public|private|active)$")
|
||||
value: (call
|
||||
function: (identifier) @_listfn2
|
||||
(#match? @_listfn2 "^list$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @declaration.name
|
||||
value: (function_definition)) @declaration.method))))))
|
||||
|
||||
;; R5 methods (inside setRefClass(... methods = list(...)))
|
||||
(binary_operator
|
||||
lhs: (identifier) @_class3
|
||||
rhs: (call
|
||||
function: (identifier) @_refFn_m
|
||||
(#match? @_refFn_m "^setRefClass$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @_methods3
|
||||
(#match? @_methods3 "^methods$")
|
||||
value: (call
|
||||
function: (identifier) @_listfn3
|
||||
(#match? @_listfn3 "^list$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @declaration.name
|
||||
value: (function_definition)) @declaration.method))))))
|
||||
|
||||
;; ── Imports ────────────────────────────────────────────────────────────────
|
||||
|
||||
;; source("path.R") — side-effect: establishes a file dependency, binds no name.
|
||||
(call
|
||||
function: (identifier) @_srcfn
|
||||
(#match? @_srcfn "^source$")
|
||||
arguments: (arguments
|
||||
. (argument
|
||||
value: [(identifier) (string)] @import.source))) @import.side-effect
|
||||
|
||||
;; library("pkg") / require("pkg") — wildcard: every top-level name of the
|
||||
;; named local package becomes visible (rProvider.expandsWildcardTo).
|
||||
(call
|
||||
function: (identifier) @_libfn
|
||||
(#match? @_libfn "^(library|require)$")
|
||||
arguments: (arguments
|
||||
. (argument
|
||||
value: [(identifier) (string)] @import.source))) @import.wildcard
|
||||
|
||||
;; ── Type bindings — R6 constructor (x <- Type$new(...)) ───────────────────
|
||||
|
||||
(binary_operator
|
||||
lhs: (identifier) @type-binding.name
|
||||
rhs: (call
|
||||
function: (extract_operator
|
||||
lhs: (identifier) @type-binding.type
|
||||
rhs: (identifier) @_newfn
|
||||
(#match? @_newfn "^new$")))) @type-binding.constructor
|
||||
|
||||
;; ── References — heritage: R6 inherit= ────────────────────────────────────
|
||||
|
||||
(binary_operator
|
||||
lhs: (identifier)
|
||||
rhs: (call
|
||||
function: (namespace_operator
|
||||
rhs: (identifier) @_r6ns_h
|
||||
(#match? @_r6ns_h "^R6Class$"))
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @_inh1
|
||||
(#match? @_inh1 "^inherit$")
|
||||
value: (identifier) @reference.inherits))))
|
||||
|
||||
(binary_operator
|
||||
lhs: (identifier)
|
||||
rhs: (call
|
||||
function: (identifier) @_r6bare_h
|
||||
(#match? @_r6bare_h "^R6Class$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @_inh2
|
||||
(#match? @_inh2 "^inherit$")
|
||||
value: (identifier) @reference.inherits))))
|
||||
|
||||
;; ── References — heritage: S4 contains= (single parent, incl. "VIRTUAL") ──
|
||||
|
||||
(call
|
||||
function: (identifier) @_s4h1
|
||||
(#match? @_s4h1 "^(setClass|setRefClass)$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @_contains1
|
||||
(#match? @_contains1 "^(contains|CONTAINS)$")
|
||||
value: (string
|
||||
content: (string_content) @reference.inherits))))
|
||||
|
||||
;; ── References — heritage: S4 contains=c(...) (multiple parents) ──────────
|
||||
|
||||
(call
|
||||
function: (identifier) @_s4h2
|
||||
(#match? @_s4h2 "^(setClass|setRefClass)$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
name: (identifier) @_contains2
|
||||
(#match? @_contains2 "^(contains|CONTAINS)$")
|
||||
value: (call
|
||||
function: (identifier) @_cfn2
|
||||
(#match? @_cfn2 "^c$")
|
||||
arguments: (arguments
|
||||
(argument
|
||||
value: (string
|
||||
content: (string_content) @reference.inherits)))))))
|
||||
|
||||
;; ── References — calls ────────────────────────────────────────────────────
|
||||
|
||||
;; Free calls: foo(x)
|
||||
(call
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
||||
;; Namespaced calls: pkg::func()
|
||||
(call
|
||||
function: (namespace_operator
|
||||
rhs: (identifier) @reference.name)) @reference.call.free
|
||||
|
||||
;; Member calls via $: obj$method()
|
||||
(call
|
||||
function: (extract_operator
|
||||
lhs: (identifier) @reference.receiver
|
||||
rhs: (identifier) @reference.name)) @reference.call.member
|
||||
`;
|
||||
|
||||
let _parser: Parser | null = null;
|
||||
let _query: Parser.Query | null = null;
|
||||
|
||||
export function getRParser(): Parser {
|
||||
if (_parser === null) {
|
||||
_parser = new Parser();
|
||||
_parser.setLanguage(R as Parameters<Parser['setLanguage']>[0]);
|
||||
}
|
||||
return _parser;
|
||||
}
|
||||
|
||||
export function getRScopeQuery(): Parser.Query {
|
||||
if (_query === null) {
|
||||
_query = new Parser.Query(R as Parameters<Parser['setLanguage']>[0], R_SCOPE_QUERY);
|
||||
}
|
||||
return _query;
|
||||
}
|
||||
86
gitnexus/src/core/ingestion/languages/r/scope-resolver.ts
Normal file
86
gitnexus/src/core/ingestion/languages/r/scope-resolver.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* R `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by the
|
||||
* generic `runScopeResolution` orchestrator.
|
||||
*
|
||||
* Thin wiring: import resolution reuses `resolveRImportTarget`
|
||||
* (`import-resolvers/r.ts`), the adapter step 2 of this plan added over the
|
||||
* existing `resolveRImportInternal`, with the repo's local R packages
|
||||
* threaded through `loadRPackageConfig`. Heritage (R6 `inherit=`, S4
|
||||
* `contains=`) rides the generic `@reference.inherits` mechanism — R6/S4
|
||||
* base names are ordinary scope-bound class names, unlike Ruby's
|
||||
* `include`/`extend`, which are method calls and need a custom
|
||||
* `emitHeritageEdges` hook.
|
||||
*/
|
||||
|
||||
import type { ParsedFile, ScopeId } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { loadRPackageConfig } from '../../language-config.js';
|
||||
import { resolveRImportTarget } from '../../import-resolvers/r.js';
|
||||
import { rProvider } from '../r.js';
|
||||
import { rArityCompatibility, rMergeBindings } from './simple-hooks.js';
|
||||
|
||||
/**
|
||||
* Enumerate all top-level names exported from a target module scope's file.
|
||||
* `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
|
||||
* scope regardless of what NAMESPACE declares as exported — verified during
|
||||
* the plan's stress-test round: `pkgA/NAMESPACE` does not export `ResultSet`,
|
||||
* yet `require("pkgA")` must still resolve it).
|
||||
*/
|
||||
function expandRWildcardNames(
|
||||
targetModuleScope: ScopeId,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
): readonly string[] {
|
||||
const target = parsedFiles.find((p) => p.moduleScope === targetModuleScope);
|
||||
if (target === undefined) return [];
|
||||
const seen = new Set<string>();
|
||||
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 (seen.has(qn)) continue;
|
||||
seen.add(qn);
|
||||
names.push(qn);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
export const rScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.R,
|
||||
languageProvider: rProvider,
|
||||
importEdgeReason: 'r-scope: import',
|
||||
|
||||
loadResolutionConfig: (repoPath: string) => loadRPackageConfig(repoPath),
|
||||
|
||||
resolveImportTarget: resolveRImportTarget,
|
||||
|
||||
expandsWildcardTo: (targetModuleScope, parsedFiles) =>
|
||||
expandRWildcardNames(targetModuleScope, parsedFiles),
|
||||
|
||||
mergeBindings: rMergeBindings,
|
||||
arityCompatibility: rArityCompatibility,
|
||||
|
||||
buildMro: (graph, parsedFiles, nodeLookup) =>
|
||||
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
||||
|
||||
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
|
||||
|
||||
// R has no `super`.
|
||||
isSuperReceiver: () => false,
|
||||
|
||||
// R's dynamic-dispatch `self$field$method()` chains and R6/S4 field access
|
||||
// benefit from the same heuristic Ruby/Python rely on.
|
||||
fieldFallbackOnMethodLookup: true,
|
||||
|
||||
// `source()` is a side-effect import (binds no name) — a cross-file free
|
||||
// call into a `source()`d file (e.g. `HelperFunc(x)`) has no lexical or
|
||||
// import binding to resolve through, so it resolves via a unique
|
||||
// workspace-wide name match instead. `library()`/`require()`'s own
|
||||
// wildcard-bound names still resolve through `expandsWildcardTo` above;
|
||||
// this only covers what that path cannot reach.
|
||||
allowGlobalFreeCallFallback: true,
|
||||
};
|
||||
49
gitnexus/src/core/ingestion/languages/r/simple-hooks.ts
Normal file
49
gitnexus/src/core/ingestion/languages/r/simple-hooks.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { BindingRef, Callsite, SymbolDefinition } from 'gitnexus-shared';
|
||||
|
||||
const TIER: Record<BindingRef['origin'], number> = {
|
||||
local: 0,
|
||||
namespace: 1,
|
||||
import: 2,
|
||||
reexport: 3,
|
||||
wildcard: 4,
|
||||
};
|
||||
|
||||
/** Local declarations shadow imports; deterministic order within a tier.
|
||||
* Same shape as Zig's `zigMergeBindings` — R has no per-scope binding
|
||||
* precedence rule beyond "the tightest-origin binding wins". */
|
||||
export function rMergeBindings(
|
||||
existing: readonly BindingRef[],
|
||||
incoming: readonly BindingRef[],
|
||||
_scopeId: string,
|
||||
): BindingRef[] {
|
||||
const all = [...existing, ...incoming];
|
||||
if (all.length === 0) return [];
|
||||
let bestTier = 99;
|
||||
for (const b of all) {
|
||||
const t = TIER[b.origin] ?? 99;
|
||||
if (t < bestTier) bestTier = t;
|
||||
}
|
||||
const byNode = new Map<string, BindingRef>();
|
||||
for (const b of all) {
|
||||
if ((TIER[b.origin] ?? 99) !== bestTier) continue;
|
||||
if (!byNode.has(b.def.nodeId)) byNode.set(b.def.nodeId, b);
|
||||
}
|
||||
return [...byNode.values()].sort((a, b) => a.def.nodeId.localeCompare(b.def.nodeId));
|
||||
}
|
||||
|
||||
/** R declarations carry no synthesized arity metadata (no `...`/default-arg
|
||||
* capture exists yet), so the comparison is always 'unknown' — a real bounds
|
||||
* check that turns on if arity captures are added later. Same rationale as
|
||||
* Zig's `zigArityCompatibility`. */
|
||||
export function rArityCompatibility(
|
||||
callsite: Callsite,
|
||||
def: SymbolDefinition,
|
||||
): 'compatible' | 'unknown' | 'incompatible' {
|
||||
const max = def.parameterCount;
|
||||
const min = def.requiredParameterCount;
|
||||
if (max === undefined && min === undefined) return 'unknown';
|
||||
if (!Number.isFinite(callsite.arity) || callsite.arity! < 0) return 'unknown';
|
||||
if (min !== undefined && callsite.arity! < min) return 'incompatible';
|
||||
if (max !== undefined && callsite.arity! > max) return 'incompatible';
|
||||
return 'compatible';
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import { dartScopeResolver } from '../../languages/dart/scope-resolver.js';
|
|||
import { vueScopeResolver } from '../../languages/vue/scope-resolver.js';
|
||||
import { zigScopeResolver } from '../../languages/zig/scope-resolver.js';
|
||||
import { objectiveCScopeResolver } from '../../languages/objective-c/scope-resolver.js';
|
||||
import { rScopeResolver } from '../../languages/r/scope-resolver.js';
|
||||
|
||||
/** Map of `SupportedLanguages` → `ScopeResolver`. The scope-resolution phase
|
||||
* iterates this map directly — every registered resolver runs. This is the
|
||||
|
|
@ -55,4 +56,5 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
|
|||
[SupportedLanguages.Vue, vueScopeResolver],
|
||||
[SupportedLanguages.Zig, zigScopeResolver],
|
||||
[SupportedLanguages.ObjectiveC, objectiveCScopeResolver],
|
||||
[SupportedLanguages.R, rScopeResolver],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -412,6 +412,21 @@ const CASES: ReadonlyMap<SupportedLanguages, ConformanceCase> = new Map([
|
|||
reachesDecoy: 'std.zig',
|
||||
},
|
||||
],
|
||||
[
|
||||
SupportedLanguages.R,
|
||||
{
|
||||
files: ['scripts/main.R', 'vendor/ggplot2.py', 'pkgA/R/utils.R'],
|
||||
fromFile: 'scripts/main.R',
|
||||
resolutionConfig: undefined,
|
||||
external: 'ggplot2',
|
||||
decoy: 'vendor/ggplot2.py',
|
||||
reachesDecoy: 'ggplot2.py',
|
||||
parsedImport: (targetRaw) =>
|
||||
targetRaw === 'ggplot2'
|
||||
? { kind: 'wildcard', targetRaw }
|
||||
: { kind: 'side-effect', targetRaw },
|
||||
},
|
||||
],
|
||||
[
|
||||
SupportedLanguages.ObjectiveC,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -546,6 +546,19 @@ const FIXTURES: ReadonlyMap<SupportedLanguages, ImportTargetFixture> = new Map<
|
|||
minimumParsedFileReads: 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
SupportedLanguages.R,
|
||||
{
|
||||
files: ['scripts/main.R', 'scripts/utils.R', 'pkgA/R/other.R'],
|
||||
fromFile: 'scripts/main.R',
|
||||
resolutionConfig: undefined,
|
||||
missTarget: (i) => `ghost${i}.R`,
|
||||
hitTarget: 'utils.R',
|
||||
parsedImport: (targetRaw) => ({ kind: 'side-effect', targetRaw }),
|
||||
minimumScans: 1,
|
||||
minimumParsedFileReads: 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
SupportedLanguages.ObjectiveC,
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue