Commit graph

1531 commits

Author SHA1 Message Date
Mick Cooney
da355bc4ca docs(r): state the R/ subdirectory limit and the re-analyse needed for layout fixes 2026-10-01 13:37:58 +01:00
Mick Cooney
551f75a9d5 test(r): pin the <name>/R/ ownership rule where a package's definitions are collected
populateRQualifiedCalls applies the fallback a second time, to decide which
files define a local package's symbols. Nothing exercised that site, so
reverting it left every unit test green. The new test fails without it.
2026-10-01 13:37:58 +01:00
Mick Cooney
c8823b566b fix(r): ignore the <name>/R/ locality fallback for files a discovered package owns
rQualifierLocality treated a qualifier as a local package when any parsed
file sat under a directory of that name followed by R/, even when discovery
had attributed the directory to a package with a different Package: name.
The directory name is now evidence only for files no discovered package owns.
2026-10-01 13:37:58 +01:00
Mick Cooney
2d3335927b fix(r): scope NAMESPACE export refinement to each package's R/ directory
refineRExportStatus matched a file against the package directory, so
functions in tests/, vignettes/ and inst/ were marked not exported although
R never loads them into the namespace, and a nested package without a
NAMESPACE inherited the enclosing package's. Ownership now comes from the
nearest discovered package whose R/ directory holds the file
(rOwningPackageDir), over every directory discovery read a DESCRIPTION in.

library() of a root-level package resolved to nothing because its directory
'' is falsy in resolveRImportInternal; it now resolves to the package's R/
files like a nested package.
2026-10-01 13:37:58 +01:00
Mick Cooney
76e3f7a110 docs(r): note that renv, packrat, revdep and .Rcheck trees are not searched for packages 2026-10-01 13:37:58 +01:00
Mick Cooney
98ac287601 fix(r): skip renv, packrat, revdep and .Rcheck trees in package discovery
Installed libraries and check output hold one DESCRIPTION per third-party
package. Discovery treated them as repository content: a renv project
library below the depth limit flagged discovery as truncated, which made
every pkg::fn() qualifier unknown, and a shallow copy became a local
package with no parsed files.

The main walk and the hidden-package scan now share one predicate that
skips a directory whose name is exactly renv, packrat or revdep, or ends in
.Rcheck after at least one character. Look-alike names are still searched.
2026-10-01 13:37:58 +01:00
Mick Cooney
fb6fbff036 fix(r): rank duplicate package directories by whole path
Among same-depth directories declaring one package name, the winner was
decided by breadth-first walk order, which compares path segment by
segment (a/x before a-b/x). Compare the whole relative path by code unit
instead (a-b/x before a/x), so the winner no longer depends on how the
walk happens to meet the directories, and update the comments to say so.
2026-10-01 13:37:58 +01:00
Mick Cooney
6f4f85bcc3 fix(r): make package discovery independent of readdir order
Package discovery used the filesystem's directory listing order, so the
200-directory cap visited different directories, and a package name
declared in several directories resolved to a different directory, on
different filesystems. Read every directory in code-unit name order and
keep the first package seen for a name: the shallowest directory, then the
path that sorts first. A root-level package is no longer replaced by a
nested copy of the same name.
2026-10-01 13:37:58 +01:00
Mick Cooney
12ba50217a fix(r): keep large exportPattern alternations and warn when a pattern is dropped
The linear-time exportPattern matcher refused anything over 1,024 states or
4,096 characters, so a legitimate `^(f1|...|f80)$` or `^[[:alpha:]]{1,2000}$`
compiled to nothing and every name in the package read as unexported, with no
diagnostic.

Raise the caps to 16,384 states and characters, and bound the hostile case by a
per-matcher work budget instead: a matcher counts the states it visits and, once
40 million are spent (about 0.4 s), answers false for names it has not decided
and reports itself exhausted. Alternations of literals are built as a trie so
alternatives sharing a prefix share states, the start closure is looked up by
code unit, closures are iterative, and verdicts are memoised per distinct name.

Every non-empty exportPattern that is dropped, and every matcher that runs out
of budget, now produces one warning per pattern per package naming the package
directory, the first 60 characters of the pattern and the reason. The dropped
patterns are also kept on RNamespaceInfo.droppedExportPatterns.
2026-10-01 13:37:58 +01:00
Mick Cooney
84d2a8be4a fix(r): match exportPattern in linear time
exportPattern() text comes from the analysed repository and was compiled
to a backtracking RegExp, so a pattern such as ^(a+)+$ took 1.6 s on a
27-character name and minutes on a slightly longer one, hanging analyze.
R itself evaluates these with the non-backtracking TRE.

Compile the pattern to a Thompson NFA and simulate it instead, so a match
costs O(name length x NFA size) for any pattern. Constructs with no
linear-time algorithm (back-references, look-around) and patterns that
expand beyond a fixed state cap are dropped like an uncompilable pattern.
Verdicts for ordinary patterns are unchanged, checked against the
previous RegExp on a generated corpus.
2026-10-01 13:37:58 +01:00
Mick Cooney
831d456069 test(r): characterise exportPattern matching
Pin the verdicts of the current exportPattern matcher for the patterns R
packages commonly use (POSIX classes, anchors, alternation, bracket
expressions, repetition, escapes) and for patterns it rejects, then
compare a seeded corpus of generated patterns against RegExp. This fixes
the behaviour a linear-time matcher must preserve.
2026-10-01 13:37:58 +01:00
Mick Cooney
c885001568 chore(r): remove remaining label pointers from code and tests
Rewrite comment and describe-title text that pointed at labels defined
nowhere in the repository and history-narrative wording that referred to
earlier work, stating each rule directly. No behaviour change; test counts
are unchanged.
2026-10-01 13:37:58 +01:00
Mick Cooney
ce5041a64c chore(r): rewrite planning-note pointers in code and tests as self-contained text
Rewrite references to planning notes (section numbers, item labels, review
rounds, scenario/unit numbering) as self-contained statements of the rule or
trade-off. Comment, fixture comment and test-description text only; no
behaviour change.
2026-10-01 13:37:58 +01:00
Mick Cooney
3dbc46f00c chore(r): make comments, test names and docs self-contained
Replace issue-number references in comments, test names and descriptions with
self-contained wording, drop merge-coordination notes from the SCHEMA_BUMP
history, and rename a fixture file to qualified_use.R. Comment,
description and file-name changes only.
2026-10-01 13:37:58 +01:00
Mick Cooney
9243c0812b test(r): register R in the import-target benchmark gate
R is registered in SCOPE_RESOLVERS but had no arm in the import-target
benchmark, so `bench/import-target/measure.mjs --check` failed its
inventory arm twice: no LANG_REGISTRY row, and a five-parameter
resolveImportTarget with no context arm.

Add the R row and its five dispatcher branches (uniqueDir, collideDir,
uniqueTarget, collideTarget, resolveOne), driving the registered hook with
library() (wildcard), source() (side-effect) and importFrom() (named, reading
the parsed workspace). R joins CONTEXT_LANGS with a probe whose two answers
differ, and HEAP_BUDGETED. baselines.json carries the R fingerprints, shape,
heap reading, context pair and budgets, with an `_r_arm` note recording the
two O(files)-per-import legs the arm measures.
2026-10-01 13:37:58 +01:00
Mick Cooney
d18d24e408 style(r): format namespace-imports and parse-worker with the pinned prettier 2026-10-01 13:37:58 +01:00
Mick Cooney
4bdacd539e fix(r): treat a completed package scan with zero packages as proving pkg:: calls external
loadRPackageConfig returned null when discovery found no package, and
rQualifierLocality maps a null config to unknown, so in a repo with no
DESCRIPTION anywhere dplyr::filter() still bound to a local filter through
the name guess. Return an empty config instead: its truncated flag now
distinguishes a complete scan (external) from one that may have missed a
package (unknown, name guess kept).
2026-10-01 13:37:58 +01:00
Mick Cooney
d2b3c2497d docs(r): document qualifier-aware call resolution and its limits 2026-10-01 13:37:58 +01:00
Mick Cooney
463626572b fix(r): flag package discovery as truncated only when a skipped subtree hides an undiscovered package
Discovery set RPackageConfig.truncated whenever it skipped any directory
(depth-3 limit) or hit the 200-directory cap, so nearly every real repo
(tests/testthat/fixtures, inst/extdata, ...) counted as truncated. Qualified
calls to outside packages were then classified unknown and kept, leaving the
dplyr::filter() bound to a local filter() case unfixed.

Skipped directories and the cap leftover queue are now collected and scanned
by hidesUndiscoveredPackage (iterative DFS, global 5000-directory budget,
skipping node_modules, .git, .Rproj.user). truncated is set only when that
scan finds a DESCRIPTION whose Package: name is not already discovered,
exceeds the budget, or hits an unreadable path; an unreadable directory in
the main walk still truncates.
2026-10-01 13:37:58 +01:00
Mick Cooney
1b53efb12d feat(r): resolve pkg::fn to local package definitions and veto contradicting name guesses
The scope query now captures the qualifier of `pkg::fn()` / `pkg:::fn()`; use it.

- qualified-call.ts (new): parseRQualifier, a tri-state rQualifierLocality
  (local / external / unknown) and populateRQualifiedCalls, wired after the
  NAMESPACE import synthesis in R's populateWorkspaceReferences.
  * external package (discovery complete): the site is dropped, so no tier
    can guess an edge for it;
  * local package with exactly one top-level definition of the name: bound
    precisely (0.85) through resolveQualifiedFreeCall, keyed by file + start
    position;
  * local package with two or more definitions: the site is dropped;
  * local package with none (re-export) or unknown locality: unchanged.
- package-config.ts: RPackageConfig.truncated is set when the directory cap,
  the depth limit or an unreadable directory left discovery incomplete, so a
  package is only called external when the scan was complete.
- isRGlobalNameFallbackPlausible: a candidate inside the named local package
  is allowed, one elsewhere is vetoed when the package defines the name.
- Flip the tests that pinned the pre-fix name-only results and drop the
  temporary pins and it.fails markers of the step-1 tests.

No shared-core changes.
2026-10-01 13:37:58 +01:00
Mick Cooney
ed9d602e80 feat(r): capture the pkg:: qualifier on namespaced calls
Capture the whole namespace_operator of pkg::fn() and pkg:::fn() calls as
@reference.qualified-name so the qualifier reaches site.rawQualifiedName and
survives the parse cache. On its own this changes no graph output: no R
consumer reads the qualifier yet.

SCHEMA_BUMP is incremented because warm ParsedFiles lack the new capture.
2026-10-01 13:37:58 +01:00
Mick Cooney
d347c14296 test(r): characterise qualified-call resolution and pin the pkg:: decoy cases
Add the r-qualified-calls and r-qualified-calls-truncated fixtures and failing-first
tests for qualifier-aware calls: the qualifier of pkg::fn() / pkg:::fn() decides the edge
(precise binding into a local package, no edge for duplicate definitions or certainly
external packages, sites kept when locality is unknown).

New expectations that encode not-yet-true behaviour are it.fails; TEMPORARY-PIN tests
assert today's edges for the same callers so an it.fails cannot pass for the wrong
reason. Existing tests are untouched; the seven that flip are listed in the header of
r-qualified-calls.test.ts.
2026-10-01 13:37:58 +01:00
Mick Cooney
1e3b680a36 test(r): assert postParse runs once for R repos and never for non-R repos 2026-10-01 13:37:58 +01:00
Mick Cooney
d902526f6b refactor(ingestion): stop writing the R-only ownerId property onto every language's definition nodes
The node-property ownerId spread in the parse worker was added with R
support and is never read for non-R languages; upstream's worker never
wrote it onto definition nodes. R never had it either (its owner is
attached post-parse). Symbol ownerId and HAS_METHOD/HAS_PROPERTY edges
are unchanged.
2026-10-01 13:37:58 +01:00
Mick Cooney
c96fc05c1b refactor(r): compute ownerNameHint in definitionPropertiesExtractor; drop dead owner plumbing
Move R's deferred-owner hint out of the shared parse worker into R's
definitionPropertiesExtractor (rDefinitionProperties), and remove the
worker-side hint block, the node/symbol ownerNameHint spreads,
ParsedSymbol.ownerNameHint and the R extractor imports. Delete the dead
hasRoxygenExport branch from rExportChecker (both branches returned true).
2026-10-01 13:37:58 +01:00
Mick Cooney
c2fcdd1bc5 refactor(ingestion): add LanguageProvider.resolveMemberOwnerNode and move R's owner-node lookup behind it
Add an optional resolveMemberOwnerNode hook consulted by the worker's
enclosing-owner helper after the container walk and resolveFileTypeOwner.
R implements it via rResolveMemberOwnerNode, replacing the two
language === R branches in parse-worker.
2026-10-01 13:37:58 +01:00
Mick Cooney
1e7985db60 refactor(r): run R post-parse through LanguageProvider.postParse
Move r-post-parse.ts into languages/r/post-parse.ts, expose rPostParse and
wire it on the R provider only. The parse phase drops its R-specific block
and relies on the generic runPostParseHooks call, so a repo with no R files
no longer reads DESCRIPTION/NAMESPACE. Flip the gating test from it.fails to it.
2026-10-01 13:37:58 +01:00
Mick Cooney
ad47dafd6e refactor(r): move RPackageConfig and loadRPackageConfig into languages/r/package-config 2026-10-01 13:37:58 +01:00
Mick Cooney
e05b0118bf feat(ingestion): add LanguageProvider.postParse and a per-provider runner
Add an optional LanguageProvider.postParse(ctx: PostParseContext) hook, the
PostParseContext type and runPostParseHooks, a pure runner beside
prepareRouteConstantsByProvider that takes a provider-lookup callback (no
registry import). It runs hooks sequentially in language-id order, awaits each
and does not isolate throws, because the hooks mutate the shared graph/model.

runChunkedParseAndResolve computes the languages present among the parseable
scanned files and calls the runner immediately after the existing R block
(same statement position: after every chunk is merged and the export-map
enrichment, before scope resolution). No provider defines the hook yet, so
this is a no-op; the R block is removed in a later step.
2026-10-01 13:37:58 +01:00
Mick Cooney
fc025b49d1 test(r): characterise deferred-owner attach, export refinement and non-R gating
Pin the behaviour that the following commits move behind LanguageProvider hooks,
before any production change:

- r-post-parse-owners (unit): attachDeferredROwners on a synthetic graph and
  semantic model, including the already-owned early return (no re-attach, no
  double registration).
- r.test.ts: r-packages HAS_METHOD 11 / HAS_PROPERTY 12, the measured edge
  triple set, 23 owned R nodes, no leftover ownerNameHint.
- r-worker-owner-absence: the compiled worker never emits ownerId for R
  (six r-* fixtures; 39 members carry ownerNameHint).
- r-post-parse-gating: a non-R repo with DESCRIPTION/NAMESPACE decoys must
  not read a manifest. Written as it.fails (the shared parse phase still
  loads the R package config for every repo) so the commit stays green; the
  R control passes today.
2026-10-01 13:37:58 +01:00
Mick Cooney
4e95057869 docs(r): correct the R capability note after NAMESPACE export support 2026-10-01 13:37:57 +01:00
Mick Cooney
b7728e72b5 fix(r): keep S3 method names from colliding with bare names in simple-name indexes
The shared simple-name indexes key every def by the text after the last '.'
of its qualifiedName, so an S3 method `print.foo` is indexed under `foo`
next to a bare `foo`. Two measured defects followed:

- `library(prov)` bound `foo` to `print.foo` (a wrong 0.85 import-resolved
  edge) when the S3 method was declared before the bare def;
- a same-package cross-file bare call `foo(v)` saw two candidates under
  `foo` and got no CALLS edge at all.

R-only fix, no shared-core edit, no SCHEMA_BUMP (both hooks run at resolve
time over cached ParsedFiles):

- rScopeResolver.isCallableVisibleFromCaller refuses dotted candidates: a
  bare R call can never invoke `print.foo`.
- expandRWildcardNames drops a bare name whose file also defines a dotted
  top-level def with that tail (order-blind, like the importFrom guard).

Characterisation tests deliberately updated to the new behaviour (r.test.ts,
R NAMESPACE importFrom fixture):
- print.tidy before tidy / tally before print.tally: was no edge, now a
  0.5 global-name-fallback edge to the bare def;
- bare call to a name that only an R6 method carries: was a 0.5 guess to
  the method, now no edge (a bare call cannot reach an R6 member).

Residual: dotted names bind only through qualified-name lookup (0.35),
never import-resolved; a bare name sharing a tail with an S3 method in the
same file resolves at 0.5 (fallback), not 0.85; backticked names unchanged.
2026-10-01 13:37:57 +01:00
Mick Cooney
7c383a31f8 fix(r): resynchronise NAMESPACE directive scanning after a malformed directive
Since the shared scanner landed, a garbled or unbalanced
directive stopped parsing the rest of the NAMESPACE file, silently dropping
every later export/importFrom directive (the old line-based export parser
resumed at the next line).

The scanner now discards the malformed directive and resumes at the next line
that begins, at column 0, with `identifier(`. To keep that safe:
- quoted strings no longer span lines, so a stray quote cannot pair with a
  later one and swallow the directives in between;
- a column-0 `identifier(` line ends any argument list still open, which also
  bounds each failed scan to the next directive rather than the end of file;
- the resume point is always strictly after the failing directive's line, so
  scanning always makes forward progress.

An indented directive following a malformed one is dropped with it. Both
parseRNamespaceExports and parseRNamespaceImportFrom benefit. No existing test
pinned stop-on-error behaviour; all pass unchanged.
2026-10-01 13:37:57 +01:00
Mick Cooney
c347764226 fix(r): apply NAMESPACE export status to root-level packages
refineRExportStatus never refined a package whose DESCRIPTION/NAMESPACE sit
at the repository root (package dir ''). Two defects combined:

- the containment test used startsWith('' + '/'), i.e. '/', which no
  repo-relative path matches;
- the result of pkgDirs.find() was guarded with `!pkgDir`, which treats the
  valid root key '' as "no package".

Treat '' as containing every path (it sorts last, so nested packages still
win) and compare the find() result against undefined. Adds a synthetic
root-package fixture, an integration test and unit tests covering export(),
exportPattern(), S3method(), unexported helpers, no-NAMESPACE, and the
unchanged nested-package behaviour.
2026-10-01 13:37:57 +01:00
Mick Cooney
3655e31b28 fix(r): parse multi-line NAMESPACE directives and unescape exportPattern strings
loadRPackageConfig read export(), exportClasses(), exportMethods(),
S3method() and exportPattern() line by line, so any wrapped directive was
silently skipped. Read them from the whole NAMESPACE text with the tokeniser
already used for importFrom(): the tokeniser is split into a shared
scanRNamespaceDirectives, and parseRNamespaceExports is a second consumer.
Wrapped, quoted, backticked, commented and CRLF directives now give the same
result as their single-line form; S3method() accepts its optional third
argument and exportPattern() accepts several patterns.

exportPattern() strings are now R-unescaped before compilation, so the R
string "\\." becomes the regex \. rather than \\. (unescapeRString handles
\\, \", \', \n, \t, \r, \a, \b, \f, \v and \x/\u/\U code points).

loadRPackageConfig changes only at the call site; no parse-cache output
changes.
2026-10-01 13:37:57 +01:00
Mick Cooney
6c0183d33b fix(r): translate POSIX character classes in exportPattern()
exportPattern() arguments are POSIX (TRE) regexes, but loadRPackageConfig
compiled them with new RegExp(), so classes such as [[:alpha:]] were read as
a JS character set and never matched. The RStudio default
exportPattern("^[[:alpha:]]+") therefore exported nothing, and named-import
export checks judged every name non-exported.

Add compileRExportPattern (languages/r/export-pattern.ts): rewrites
[:alpha:], [:alnum:], [:digit:], [:upper:], [:lower:], [:punct:],
[:space:] (plus blank, cntrl, print, graph, xdigit) inside bracket
expressions to ASCII JS equivalents, handles negation and mixed ranges, and
returns null for unknown classes or unterminated brackets. Patterns without
"[:" compile verbatim, so existing behaviour is unchanged. Compilation still
happens once per pattern in loadRPackageConfig.

Deliberate test change: the "POSIX exportPattern characterisation" in
r-namespace-imports.test.ts pinned the wrong behaviour (a JS-compiled POSIX
pattern refuses every name). It now asserts the corrected behaviour, plus a
non-matching control.
2026-10-01 13:37:57 +01:00
Mick Cooney
f90484c296 test(r): add R grammar and sample-code coverage
Add sample-code/simple.r and an R block in tree-sitter-languages covering
function, S4, and R6 definitions, library()/source() imports, and free,
member, namespaced, and native-pipe calls. List simple.r in the parsing
fixture-existence check and add R extension detection to ingestion-utils.
2026-10-01 13:37:57 +01:00
Mick Cooney
4954ec2945 docs(r): add R to language support tables
Add R to the language lists and feature matrices in README.md and
gitnexus/README.md, with an honest capability note (experimental,
name-based fallback for same-package bare calls, no export() handling).
Correct the ARCHITECTURE.md language and provider counts from 18 to 19.
2026-10-01 13:37:57 +01:00
Mick Cooney
0c16954425 fix(r): clear eslint errors in changed files 2026-10-01 13:37:57 +01:00
Mick Cooney
b0fc635475 style(r): format changed files with prettier 2026-10-01 13:37:57 +01:00
Mick Cooney
3db4fc07e4 chore(r): drop stray tree-sitter-cpp nested node-addon-api lockfile entry
The nested node_modules/tree-sitter-cpp/node_modules/node-addon-api 8.7.0
entry is unrelated to the R grammar. The hoisted 8.9.2 already satisfies
tree-sitter-cpp's ^8.2.1 range; npm regenerates the lockfile unchanged.
2026-10-01 13:37:57 +01:00
Mick Cooney
be43c509bb test(r): register R in callable-value-flow coverage and parser-loader-abi smoke cases 2026-10-01 13:37:57 +01:00
Mick Cooney
c415f35812 feat(r): veto global-name fallback guesses contradicted by importFrom
Refuse a unique-name global fallback edge when the caller package NAMESPACE
imports the name (last importFrom entry wins) from a package other than the
one the guess landed in and other than the caller's own package. Callers and
candidates outside a package R/ directory, local providers that re-export the
name, self-imports and undecidable configs keep the baseline edge. The
pkg::name() and import() limitations are pinned by characterisation tests.
2026-10-01 13:37:57 +01:00
Mick Cooney
49db332589 feat(r): bind NAMESPACE importFrom names to local package definitions
Synthesise a `named` import for every NAMESPACE importFrom(pkg, name) whose
pkg is a local package, into each R/ file of the importing package
(populateRNamespaceImports, wired via populateWorkspaceReferences with
postExtractSourceTextPolicy 'uncached-files'), and resolve it in
resolveRImportTarget's named branch.

Rules: last entry wins per name over all entries; a self-import, an
external last entry, and a name the importing package defines at top level
(own namespace masks) synthesise nothing. The named branch builds no
workspace file index, returns null for external or unknown packages, dotted
names (finalize keys defs by the text after the last dot), providers whose
NAMESPACE does not export the name (backticks stripped), names with no
top-level definition, and a defining file that also holds a dotted def
whose tail equals the name. Top-level definitions are read from the Module
scope's hoisted bindings, not ownedDefs/localDefs.

Known limitations, pinned by tests: pkg::name() qualifiers are discarded
at this point, a nested same-named function can still win the binding, and
POSIX-class exportPattern() never matches in a JS RegExp.
2026-10-01 13:37:57 +01:00
Mick Cooney
83571b66a2 feat(r): bind named imports to top-level definitions only
Set namedImportsBindTopLevelOnly on rScopeResolver so a named import can
only bind a module-level definition of the target file, never an R6/R5
member with the same simple name. Groundwork for NAMESPACE importFrom()
binding; r.test.ts (127) is unchanged by the flag alone.
2026-10-01 13:37:57 +01:00
Mick Cooney
a437b04c06 feat(r): parse NAMESPACE importFrom() directives 2026-10-01 13:37:57 +01:00
Mick Cooney
e9be500739 test(r): native pipe chain fixtures and resolution tests 2026-10-01 13:37:57 +01:00
Mick Cooney
31e006fb5e fix(r): anchor function scopes on assignment and method-argument nodes
Every R call site's CALLS source is now the callable that lexically contains
it (named function, nested named function, R6/R5 method), or the File node when
no named callable encloses it; a call in a class body outside any method (an
R6 field default, a setClass validity body) is sourced from the Class node via
the unchanged resolveCallerGraphId class-anchor fallback.

Cause: R's @scope.function anchored only on the function_definition, while
@declaration.function / @declaration.method anchor on the enclosing assignment /
named argument. The def was therefore owned by the module / outer-function /
Class scope, and resolveCallerGraphId credited every call in a body to the FIRST
callable owned by that scope (false inner -> inner self-loops; module-level
calls credited to a function instead of File).

Fix (languages/r/query.ts only; no shared pipeline change, no @declaration.*
change so def ids and resolveDefGraphId reconciliation are untouched):
- (binary_operator lhs: (identifier) rhs: (function_definition)) @scope.function,
  directly beside the @declaration.function pattern it must track.
- (argument name: (identifier) value: (function_definition)) @scope.function,
  directly beside the @declaration.method patterns; generic, so it cannot drift
  from the three R6/R5 nesting patterns, and inert for non-method arguments.
- Both carry a "must stay in lockstep" comment; the blanket function_definition
  scope stays (anonymous functions); the query header rationale is rewritten.

Cache: SCHEMA_BUMP is incremented (ledger comment added, pin test updated). Warm
ParsedFiles and durable stores hold the pre-fix scope tree.

Tests: removes every it.fails wrapper from the tests added in the previous commit
(18 unit, 36 integration) and deletes the pins of the old, defective behaviour
(5 unit, 4 integration);
they now pass as ordinary tests. No other assertion was changed.

Verification (built dist): scope-function-anchors 42/42, r.test.ts 97/97
(39 existing + 58 new), incremental-parse-cache, call-arguments-r,
external-import-conformance, import-target-index-reuse.contract, the three
grammar-literal gate files, tsc --noEmit clean.
2026-10-01 13:37:57 +01:00
Mick Cooney
22ffcd3220 test(r): caller-attribution scope-anchor unit tests and fixture (expected-fail)
Adds the tests for the R caller-attribution fix BEFORE the fix, so the fix commit only flips them to normal tests.

Defect: R's @scope.function anchors on the function_definition while
@declaration.function / @declaration.method anchor on the enclosing assignment
or method argument. The def is therefore owned by the Module / outer-function /
Class scope, and resolveCallerGraphId credits every call in a body to the FIRST
callable that scope owns (false inner -> inner self-loops, R6 calls credited to
the class's first method, module-level calls credited to a function not File).

Contents:
- test/unit/scope-resolution/r/scope-function-anchors.test.ts: scope-tree proofs
  via extractParsedFile + populateClassOwnedMembers (named forms, nesting,
  anonymous lambdas, R6/R5 methods, nested helper in a method, class-body field
  default, single-function file, member-assigned functions, def node id pins).
- test/fixtures/lang-resolution/r-call-attribution/: synthetic fixture, a decoy
  first function per file, unique leaf callees.
- test/integration/resolvers/r.test.ts: new 'R caller attribution' describe with
  exact per-source edge sets and named-type invariance counts.

Assertions that fail at HEAD because of the misattribution are wrapped in
it.fails (one assertion per it, value-comparing, no non-null derefs) and are
unwrapped by the fix commit; assertions that already hold stay plain; each
wrapped case has a plain twin (the target edge exists from any source). The
step-1-only HEAD pins assert the defect itself and are deleted by the fix
commit. Verified: every wrapped test fails without the wrapper at HEAD with an
assertion mismatch (no throws) and passes when the query is patched with the
two scope patterns; only the HEAD pins flip.
2026-10-01 13:37:57 +01:00
Mick Cooney
0eab534e1b fix(r): register R in grammar-literal-validation gate's language maps
R was missing from BASENAME_LANGS/DIR_LANG (literal-collectors.ts) and
GRAMMAR_PACKAGES (grammar-introspection.ts), so literals in field-
extractors/r.ts, method-extractors/r.ts, and type-extractors/r.ts were
tagged with every OTHER language's grammar instead of R's, and the new
languages/r/ resolution-layer files (scope-resolver, query.ts, etc.)
were skipped entirely by the gate's Mode 3/4 scans. `namespace_operator`
and `extract_operator` are valid R node types, so this was a mistagging
bug in the test helpers, not a dead grammar literal.
2026-10-01 13:37:57 +01:00