The helper listed only 'name <- function', while the indexer also lists
<<- and = definitions, so a generic defined with = never reached the S3
guard. Add cases that go through the helper with each operator.
rMergeBindings kept the first binding of a node, so a batched call with a
package named by several library() calls was order-sensitive. Keep the
binding whose call starts later, add a unit case and pin the ABA and BAB
shapes through the pipeline.
rename-provider-r.test.ts was charged 100 s in WINDOWS_WEIGHTS_SEC but
takes 600-750 s on the Windows runner (49 persisted fixtures, each a
native analyze). The shard packer therefore placed it on shard 5/10 with
the go and dml-gate suites, which exhausted the 20-minute watchdog on
two consecutive attempts.
Charge it 620 s, add a regression test that keeps it off the go and
dml-gate shards, and raise the recorded-workload ceiling from 17 to 18
minutes: the table total grew by about 520 s, so a perfect ten-way
split now averages about 1047 s per shard.
* fix(rename): bind apply to reviewed plans (U7)
Retain exact semantic edits behind single-use preview tokens and reject
source, target, inventory and graph drift before application. Preserve
explicit partial-write accounting and keep large unedited sources covered
with hashes instead of retained text.
Refs #2973
* fix(rename): cover unindexed sources (U7)
Address PR #3617 review by binding every supported source to streamed byte
hashes and canonical targets, including tracked and untracked files absent
from the graph. Keep the exact reviewed edits and bound evidence memory.
Use conventional even-sample medians and extend the existing CI benchmark
to capture and validate unindexed sources without relaxing timing gates.
Note: pre-existing packed Tree-sitter GLIBCXX_3.4.31 host failure in
parse-cache-identity-package.test.ts is not addressed by this PR.
* fix(rename): preserve lazy startup (U7)
Keep language providers off the MCP startup path; load their existing
classification only when rename inventory is requested. The existing
12 startup closure checks now pass.
Address PR #3617 review by including token validation in reviewed apply
timing and growing indexed evidence before preview, preserving the
20MiB controls under a 256MiB native pool. Migrate dispatch and retention
tests to the preview token contract, keeping write-failure oracles.
Note: pre-existing auto-sync /tmp fixture security and installed ARM64
grammar-header failures remain outside this PR; the packed Tree-sitter
GLIBCXX_3.4.31 host failure also reproduces on unchanged main.
* test(rename): always remove apply fixture (U7)
Own the temporary directory from creation in an outer finally so setup
and preview assertions cannot bypass cleanup. Keep spy restoration in
its inner finally and preserve all write-failure assertions.
Address PR #3617 cleanup feedback.
* fix(rename): retain targets and config evidence
* test(rename): verify approved physical output
* test(rename): cover native filesystem matrix
* test(rename): align preview contract fixtures
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
r-provider.md documents this as a limitation of the shared incremental write path. Pin the
current behaviour (stale after an incremental analyse, correct after a forced one) so a fix
there shows up and the document is updated with it.
Control flow inside an expression (y <- if, switch, &&, ||, ifelse, return() or break inside
tryCatch/local), stop(), on.exit() and tryCatch handlers stay one block, and a break or next
outside a loop falls back to EXIT. Nothing exercised these forms.
The documented claim that a rename succeeds across files had only same-file successes behind
it, and the undetected paste0() lookup was unpinned. Add a qualified pkg::name() cross-file
rename and a run-time-built lookup name, and count the R cases so that an emptied fixture
directory cannot leave the suite green with no test.
Add the r-call-shapes fixture, a package that contains each shape r-provider.md says creates
no edge or node: lapply/Map/do.call/match.fun/get/registry/pkg::f as values, infix operators,
obj@slot, UseMethod/NextMethod, super$ and private$, inherit = pkg::Base, an inherited
self$ call, setClassUnion/setValidity/setReplaceMethod/Class$methods, roxygen @export,
exportClassPattern, renv parsing and the unread import forms. Positive controls sit beside
them, and the whole CALLS set is pinned.
The layout fixture already holds callers in tests/, scripts/, vignettes/ and inst/, but only
their export status was asserted. Pin every CALLS edge, with reason and confidence, for both
projects, so a misdirected edge that keeps the count fails.
The deferred-owner pass and the NAMESPACE export refinement only ever ran cold in the R
tests. Analyse r-packages and r-package-layout twice, the second time from the saved parse
cache with no worker pool, and require the same definitions (isExported, ownerId) and the
same CALLS, EXTENDS, IMPORTS and member edges.
rArityCompatibility had no test at all. populateRQualifiedCalls and
rFileBelongsToDiscoveredPackage are reached with no package config, and a run must not see
the previous run's answers; neither was asserted. The npm rebuild hint for the R grammar
names the scoped package; nothing checked it.
R matches argument names case-sensitively, so setClass("A", CONTAINS = "B") names no
base; the scope query linked it anyway. Drop the CONTAINS spelling from both predicates.
Pin the VIRTUAL placeholder: a class naming both a real base and VIRTUAL gets both edges
(the dedup set must be seeded only from edges that target the placeholder), the edge keeps
its confidence and reason, and running the heritage hook twice mints one node and one edge.
The caps in r-provider.md were asserted through the constants themselves, so changing a
cap, or the number the document gives, failed nothing. Pin each cap, check that the
document states the same number, and test the state cap, the pattern length cap, the
hidden-package scan budget and a spent matcher at their exact boundaries. Also pin the
backslash-u fallback to the letter u when fewer than four hex digits follow.
Each statement was reproduced against the code before it was written: self$m()
without a type binding, RefClass sibling calls, calls to generics and
generators, source() suffix matching, the library() edge fan-out, same-named
helpers in two packages, METHOD_OVERRIDES on parameter counts, calls inside
setMethod bodies, inherit = pkg::Base, string-named R6 members, nested function
ids, the TRE differences in export patterns and conditional NAMESPACE bodies.
Also says how a class name shared by several classes is owned and reported.
rExportChecker was a shared export that only the R provider used, and it made
export-detection.ts differ from upstream for a function that returns true.
ObjectiveC already writes the same checker inline.
The provider now carries it as () => true with the reason beside it, and
export-detection.ts is byte-identical to upstream again.
extractParameter runs for each formal and re-collected and re-parsed the whole
comment block above the function every time, so work grew as formals x block
lines (2,000 formals under 2,000 @param lines took 11.9 s).
The parsed block is now kept per assignment node in the tree, as the file's
class names already are.
The R provider header said library() and source() bring everything into scope
with no named imports and that imports are standard import nodes: source()
binds no name, NAMESPACE importFrom() produces named imports, and the import
captures come from the R scope query. The scope resolver said a bare member
call never reaches an R5 method and that R has no super, but RefClass methods
call siblings by bare name (unresolved today) and R6 has super$method().
The tree-sitter-grammars group matched tree-sitter-* only, so
@eagleoutice/tree-sitter-r would have been bumped in a pull request of its own
instead of with the other grammars.
The scope-resolution section says adding a language needs no changes to shared
code. R added two optional LanguageProvider hooks (postParse and
resolveMemberOwnerNode) that no other language implements, so the sentence now
names that case and points at the hook table.
rQualifierLocality scanned every parsed file path once per distinct pkg::
qualifier, so a run's work was qualifiers x files (4,000 x 5,000 took 0.84 s).
populateRQualifiedCalls now builds the set of <name>/R/ directory names that no
discovered package owns once, and each qualifier is a set lookup. The answers
are unchanged, and a call without the index still scans.
A formal or a for variable bound no name in the scope model, so
function(data, transform) transform(data) was credited with a call of a
top-level transform() at 0.85, and the unique-name fallback made the same guess
across files.
Each function scope now publishes the names its formals and for loops bind as
lexical names, the shadowing barrier the scope model already has for a
parameter. A formal with a constant default that the body never assigns is left
out, because R skips a value that is not a function when it looks up a callee
(the rename provider reads it the same way).
Two library() calls that attach packages defining the same name bound the call
to whichever package file sorted first by id, so a script that attached lp2
after lp reached lp's function while R reaches lp2's.
rMergeBindings now keeps, within the wildcard tier, only the bindings of the
library() call that comes last in the file. A package of several files that
defines the name twice stays ambiguous, and a binding with no call position
leaves the tier as it was.
A function bound inside another function lives in that call's frame, so no
other function in its file or another can call it by its bare name. The
unique-name fallback ignored that: when the name was unique in the repository,
a bare call elsewhere bound to the closure-local function at 0.5.
The R fallback veto now refuses a function or class that the file's scope
model holds below the top level. A candidate the scope model does not hold, and
every top-level definition, are judged as before.
Resolving an importFrom() name that the provider's NAMESPACE does not export
walked every export entry before giving up, so a provider with 20,000 exports
and a caller importing 20,000 names took about two seconds, and NAMESPACE text
is the only bound.
The walk only looked for entries that start or end with a backtick (a quoted
"`%+%`" entry keeps them). Those are now collected in one pass per export set,
and a lookup is a set membership test; the answers are unchanged.
Discovery read the disk on its own and skipped only renv, packrat, revdep and
.Rcheck, so an installed library under lib/, library/, .Rlibs/, vendor/ or
build/ registered its packages as repository packages (their qualified calls
then fell to the name-based guess instead of reading as external), and a tree
of ignored directories spent the 5,000-directory hidden-package budget and
made discovery read as incomplete.
Discovery now skips dot-directories and the directories the indexer itself
ignores (isHardcodedIgnoredDirectory), and a DESCRIPTION with a Built: field,
which only an installed package has, registers nothing and hides nothing. The
depth-3 and 200-directory bounds are pinned with their boundary values.
S3method(pkg::generic, class), the delayed-registration form roxygen2 writes
for @exportS3Method pkg::generic, registered no method at all when written
bare and the nonexistent "pkg::generic.class" when quoted, so the function
generic.class read as not exported and an importFrom() of it did not bind.
The generic is the text after the last :: or ::: outside quotes and
backticks, so the method is generic.class as R registers it.
A pattern that expanded past the state cap was refused only after building up
to 16,384 states, yet it reserved no states, charged no work and did not close
the allowance, so the next such pattern started from scratch: 98 KB of
exportPattern("(a|b){8000}...") lines took 4.2 s while the budget counted
nothing, against the documented bound of about a second.
A refusal is now charged for what was built (eight visits per state or build
step), and a spent work budget refuses later patterns before they are parsed.
The guide also says that budget exhaustion while resolving importFrom() names
is not reported, because the scope resolver loads its own configuration.
A matcher answered a name it had already decided from its memo without any
charge, so asking thousands of matchers about the same few names (initialize,
print, show across many classes) cost time that no budget counted: 5,000
repeat calls over 12,000 matchers took 612 ms and charged nothing.
A memo hit now costs one visit, so the repeat is bounded by the shared budget
like every other cost, and the verdict itself is unchanged.
MAX_NESTING was 2048, beyond what a cold process can recurse: a deeply nested
pattern failed with a stack overflow in a fresh process and compiled after a
warm-up, so whether a NAMESPACE pattern was in effect depended on the machine
and on what ran before it, and the reason given was misleading.
The limit is now 256, which still compiles on a quarter of the default stack,
so the verdict is the same cold or warm and carries the right reason.
setMethod("show", signature("A"), ...) and c("A", "B") signatures named no
owner, so the methods of one generic on different classes collapsed into one
node and none had a HAS_METHOD edge.
The owner is the string in the first argument of a signature() or c() value,
named or not. A multi-class signature therefore belongs to its first class; a
first argument that is not a string still names no owner.
attachDeferredROwners left every member of a class unowned when more than one
class of that name was registered, including a class defined twice in one file
(one graph node registered twice) and the same helper class in R/ and tests/.
Candidates are now de-duplicated by node id. When several distinct classes
share the name, the member attaches to the one defined in its own file; if its
file defines none or several of them the member stays unowned, as before, and
one warning per call names the class names affected.
The rename section now names each refusal the provider makes (NAMESPACE export
read from the indexed flag, infix functions and operators, replacement functions,
new-name collisions and value reads, group and common generics). The control-flow
section records that pdg_query flows, explain and impact --mode pdg return empty
answers for R without saying that R has no def/use harvest; the shared tools carry
no per-language capability signal, so this is documented rather than patched.
Four unit files handed checkSafety a graph whose forEachNode did nothing, so the
NAMESPACE branch could never be reached from them. They now build a real graph
(empty, or with one node of a chosen label for the member-versus-function case).
BASE_GENERICS missed the S3 group generics and many dispatching base functions, so
sum.items, Ops.money, Math.money, abs.diff, is.numeric.foo, as.complex.foo and
unlist.foo were accepted and silently registered as methods.
The list now has the Ops, Math, Summary, Complex and matrixOps groups with each
member, the is.* and as.* generics, and the common base, stats, graphics and utils
generics. Unit tests walk the documented group members, and two fixtures cover
Ops.money and sum.items.
The engine protects the call form of a new name, but R's reference sites are calls,
so a plain read was invisible: sapply(x, paste), Reduce(max, x), x <- letters or a
default f = assist all kept compiling and silently started to mean the renamed
global.
The provider now refuses any identifier read of the new name that is not bound
locally: a parameter of an enclosing function covers a read, and covers a call only
when it has no default. Bound names, argument names, members ($, @) and qualified
names are not reads. Four fixtures and unit tests cover values, callees, formulas
and the allowed spellings.
assertDeclarationCollisions is per file and function-only, so a rename onto a name
that a top-level variable already held (or assigned later) overwrote or was
overwritten by it, and a function of that name in another file became a second
global whose survivor depended on load order.
The provider now refuses a new name that is assigned at top level (<-, =, ->, a
for variable), assigned with <<- or ->> anywhere, or defined as a function in
another file. Local assignments, parameters and members of that name are still
allowed. Five fixtures and a positive control cover each form.
helper(z) <- 5 calls the replacement function helper<-, not helper, so renaming
the getter renamed the call in the assignment target and left the setter
unreachable under the new name, and renaming helper<- itself left every
helper(z) <- v calling a function that no longer exists.
Refused now: a call of the name anywhere in an assignment target (plain, nested
in $, [[, names(), attr(), or the -> forms), an old or new name ending in <-, and
a string-defined setter of either name. Four fixtures and a unit test cover each
form beside calls that are only read.