roxygen2 rebuilds NAMESPACE from @export* tags, so renaming a tagged function
changes the package's public API on the next document() even when the NAMESPACE on
disk and the persisted isExported say it is private. checkSafety now refuses a
top-level function whose directly preceding #' block has an @export* tag. Documents
that a successful rename leaves .Rmd/.qmd/.Rd/pkgdown/roxygen cross-reference
consumers unchanged; the rename result has no provider-owned note field.
parse-worker.ts and languages/r/query.ts imported @eagleoutice/tree-sitter-r
statically. A binding that cannot load (no prebuild, ABI mismatch) therefore
crashed every worker and every module importing the R provider, failing analyze
for all languages. The worker now loads it behind the same guard as the vendored
grammars, through the shared grammar loader, and the query module resolves it on
first use as the Kotlin one does. With the binding removed, analyze indexes the
other languages and skips R files with one logged error.
The NAMESPACE scanner skipped conditional (if/else) bodies, dropped raw-string and
multi-line arguments, and discarded a malformed directive together with everything up
to the next column-0 directive, all silently. Rename treated 'not in the export lists'
as proof of privacy, so it could rename a function whose export() entry the reader had
never seen and leave NAMESPACE naming a missing function.
The scanner now reports when a skipped or discarded region names an export-family
directive, and parseRNamespaceExports also flags an argument of one that it cannot
read. loadRPackageConfig stores that as RNamespaceInfo.exportsUnreadable and rename
refuses with incomplete_semantics. An ordinary NAMESPACE, including conditional
imports, does not set it. The indexed isExported is unchanged. r-provider.md updated.
After an edit of NAMESPACE alone, the unchanged R/ files are not
re-processed, so the persisted isExported of a newly exported function
stays false and rename read that as proof of privacy. The rename guard now
also uses the NAMESPACE the planner has just loaded from disk (recorded by
the R workspace hook against each parsed file): it refuses when NAMESPACE
exports the name by export(), exportPattern() or S3method(), and fails
closed (incomplete_semantics) when NAMESPACE is gone or unreadable, when
an exportPattern() was dropped or ran out of budget, or when no config was
recorded. The persisted flag remains a second, additive check. The
incremental test now asserts the refusal on the stale index and after
--force; unit cases cover each way NAMESPACE can change. Docs updated.
R leaves an already attached package where it is, so library(a);
library(b); library(a) still lets b mask a. The merger counted the last
call as a fresh attachment, and because the pipeline folds the bindings of
a name in one at a time, a package masked at one step was forgotten at the
next. Remember each package's first attachment, masked ones included, on
the surviving bindings, and flip the expectations that pinned the other
direction (A/B/A -> B, B/A/B -> A) in the unit and pipeline tests.
validateName let ... and ..N through to a misleading name_collision. Reject
them as reserved, say in the message that new names are ASCII-only by design,
and report an old name made only of non-ASCII characters as an unsupported
identifier rather than an infix function or operator.
A formal defaulting to NULL is the usual placeholder of an optional
callback, so classing it as constant let a rename edit the call of the
parameter and drew a false edge to a same-named top-level function. Treat it
as 'maybe': the rename refuses the call and the scope publishes the formal
as a lexical name. Update the provider guide and pin both paths.
rExportPatternMatches answered false for every name once the budget was
spent, so a duplicate definition of a name that had been exported earlier
flipped to private. Remember the names matched while the budget lasted and
answer them from that set; undecided names still read as unexported, which
the documentation now says.
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.
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.
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.
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.
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.
The guide documents ambiguous_reference for an unqualified call into
another file and a refusal for R6 methods, but no fixture covered
either. Add both. The R6 method is refused by the provider as a class
member (unsupported_occurrence); the guide wrongly listed it among the
shared engine's stale_graph refusals, which now name only dotted
functions and S4 classes.
The guide said 18-provider registry. The providers table in
languages/index.ts has 19 entries with R, one per SupportedLanguages
value (counted from the built registry: Object.keys(providers).length).
R skips a binding that is not a function when it looks up the callee of
f(x), but the rename provider claimed every parameter, so a call under
function(x, helper = 2) was left unrenamed and failed after the rename.
A parameter whose default is a constant and which the body never assigns
is no longer claimed, so the call is renamed with the function it
reaches. A parameter that may hold a function (a function or computed
default, or an assignment to it in the body) is claimed as before, and
a call of the name under it is refused as ambiguous_reference, since it
may reach either. A parameter without a default that is called stays a
function-valued parameter and its call is left alone.
A <<- inside a function is a global re-definition that the scope graph
keeps as a second declaration of the same name. A rename dropped the
selected function and its calls but left the superassignment, so after
the rename the function wrote a new global of the old name instead of
replacing the renamed one. The top-level form was already refused by
the engine; checkSafety now refuses any <<- or ->> to the name, in or
out of a function, with the same ambiguous_reference code.
The guard looked only at the text before the first dot, so
to.json.foo (a method of the dotted generic to.json) passed, and base
generics such as print, format, summary and plot were never checked
unless the sources defined them. Every dot-separated prefix of the new
name is now tested against the functions the indexed sources define and
a small list of base generics.
A function written in backticks keeps them in its binding key and on its
node, while NAMESPACE entries and importFrom() carry the bare symbol, so
importFrom(pkg, `%||%`) found no defining file and the call degraded to
the name-based guess, which cannot tell two packages' `%||%` apart.
rFileTopLevel now keys the top-level names by symbol (rBareName) and
records the spellings it saw. resolveRNamedImport looks files up by
symbol, populateRNamespaceImports synthesises the import with the
definition's own spelling (one per spelling the provider uses) and lets
a backtick-defined function of the caller package mask its import. The
global-name-fallback veto and the pkg::name() lookups compare symbols.
The dotted-tail guard still compares the name as written, which is how
finalize indexes it. Names without backticks answer exactly as before,
pinned by a characterisation snapshot recorded before the change.
A call and its definition must spell a backticked name alike to be linked,
and importFrom() of a backtick-defined function does not bind. NAMESPACE
exports and rename match the symbol either way.
The slot pattern matched only list(...) and representation(...), although
the field extractor accepts any call, so setClass("A", slots = c(a =
"numeric")) and setRefClass("R", fields = c(...)) created no Property nodes.
Accept c(...) for both. An unnamed string in slots = c("a", "b") or
fields = c("a", "b") names an untyped member, as in R; an unnamed string in
representation(...) or list(...) is still not a member. The new pattern
captures its callee as @_vecOwner, so the class-naming capture filter does
not mistake these strings for the class name.
Member ids carried no owner, so a second class in one file that shared a
member name with an earlier one (push, items, a setMethod generic, a slot)
minted the same id as the first and lost the member, along with its
HAS_METHOD or HAS_PROPERTY edge.
Register resolveDetachedOwnerName for R. It names the class declared by an
ancestor R6Class assignment, setClass or setRefClass call, or the signature
class of a setMethod call, so ids become Method:<file>:Stack.push#n and
Property:<file>:Stack.items. The owner edge is still attached by postParse
through ownerNameHint. The change is R-only: the hook is optional and
undefined for every other language.
The r-packages characterisation counts now include Child.greet, the member
the id collision used to drop.
r-provider.md: state that an empty or missing if arm is the header's seq
edge (control dependence labels it as the complement of the other arm);
extend the bare-callee limit to library(), require() and source(); say that
NAMESPACE raw strings and strings spanning lines are dropped without a
warning, which the logged-warning sentence did not cover; name the language
registrations that do mention R; give the devtools::load_all() rationale for
the unfiltered library() wildcard.
type-resolution-system.md: R binds no declaration, call-result or
method-call-result types (extractDeclaration is empty and there is no
extractPendingAssignment), and its cross-file propagation is partial, with a
test cited. Drop R from the footnote about synthesizeWildcardImportBindings(),
which does not exist.
The R rename guard compared the raw text of each `string_content` node with the
old and new name. An escaped spelling (`"f\u006fo"`, `'f\x6fo'`, octal) keeps
its escape inside string_content, and a raw string (`r"(foo)"`) has no
string_content node at all, so renaming `foo` proceeded while a get() or
do.call() on the escaped or raw spelling still resolved the old name.
Reproduction: 10 cases in the new test/unit/r-rename-string-guard.test.ts
returned 'ok' instead of unsupported_occurrence on the unfixed provider, and the
two new fixtures string-lookup-escaped and string-lookup-raw (rename-provider-r)
renamed successfully on the unfixed build. The guard now decodes each `string`
node (escapes, octal, raw strings) and compares the value.
docs/languages/r-provider.md said "spelled as a string literal"; it now states
the whole-value rule and what is still not detected (substring, paste0(),
non-indexed files).
Review finding: `methods::setClass(...)` is ignored by the S4 field detector.
Confirmed by test, and wider than fields: every S4/RefClass pattern
(setClass, setRefClass, setGeneric, setMethod in R_QUERIES, plus the field and
method extractors) requires a bare identifier callee, so a namespace_operator
callee creates no class, slot, generic or method. R6::R6Class is supported.
Widening touches nine files (queries, naming-argument, captures, owner hooks,
post-parse, scope resolver, both extractors) and the qualifier-veto logic, so
it is not a trivial change. Documented as a known limit in
docs/languages/r-provider.md and pinned by characterisation tests in
r-definition-anchors.test.ts (each paired with its unqualified control).
Plain R functions rename through the scope graph; S3, string-spelled,
S4, R6 and NAMESPACE-exported names are refused. Adds fixtures, the
rename-provider-r test and its cross-platform shard entries.
populateRNamespaceImports synthesised a named import for every
importFrom(localpkg, name). Names the provider does not export, does not
define at top level, spells with a dot, or defines beside a dotted S3
method of the same tail never resolve, yet the unresolved import counted as
an explicit name claim and suppressed the documented 0.5-confidence
global-name-fallback edge.
Skip the import when resolveRNamedImport returns null, so such names create
no phantom claim and their calls keep the fallback.
Upstream took SCHEMA_BUMP 133 to 137, so the R provider bump moves to 138.
The pin test and the taken-versions list are updated to match. The import-target
benchmark keeps both the R and Rust arms in the heap-budgeted and context
language lists, and the tree-sitter readiness tests keep the R grammar checks
against the reworked report (R is now a peer-patch grammar and the held-grammar
counts changed).
S3method(generic, class, method) registers method, not generic.class, as the S3
method. parseRNamespaceExports ignored the third argument and so marked a
function called generic.class public while the registered method stayed
non-exported. Use the third argument when it is a plain name, else generic.class.
R matches named arguments first, so in setClass(a = "Q", "B", Class = "A") the
class is A. The naming-argument scan stopped at the first unnamed argument and
named both B and A. Keep scanning the list, once and cached, and drop the choice
when an argument spelled with the formal appears. Replaces the pinned known-limit
test and the known-limit wording in the file header and the provider guide.
* docs: add Objective-C fork provider notes
* feat(objective-c): add deterministic provider and grammar
* feat(objective-c): finalize provider MVP
* fix(objective-c): harden provider integration
* fix(objective-c): normalize bare macro markers
* docs(objective-c): integrate provider documentation
* fix(objective-c): harden resolution and header classification
* fix(objective-c): complete provider follow-ups
* fix: address Objective-C review follow-ups
* chore: format Objective-C grammar sources
* fix(objective-c): harden review follow-ups
* Address PR review feedback (#3179)
Keep Objective-C chunking and macro recovery aligned with the grammar, and stop Community MEMBER_OF edges from leaking into symbol context.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address follow-up review on ObjC chunking and language fallback.
Keep preprocessor directive text from changing file-scope brace depth, group real ivar nodes, skip header modifiers, and restore Rakefile/Gemfile detection through getLanguageFromFilename.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Parse Objective-C headers with the objc grammar in embeddings.
ensureAndParse and structural extraction now use the same content classifier as ingest, including method snippets from .h files, so Protocol/Category/Class chunks are not re-parsed as C++.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3179)
Keep file-scope macro elision off C line splices and @interface/@protocol/@implementation bodies, and attach ivar attributes to the following instance variable when chunking.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(bench): rebaseline Objective-C CSV emit
* feat(objective-c): add workspace resolution and linear emit benches
Plain .h files are classified as C++, so the ObjC pass could not
resolve #import of those headers. Load a C/C#-style workspace once
per pass, and keep protocol-candidate USES linear.
Refs #3179
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3179)
- Compare LadybugDB labels() as a scalar when excluding Community MEMBER_OF edges.
- Walk superclass members, skip file-static C sibling defs, and ignore comments in ObjC header/macro scans.
Note: pre-existing failure in objective-c-provider integration (worker-pool ready timeout) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3179)
Emit Objective-C declaration captures so compilation-unit siblings can share
header/implementation bindings, and keep class vs protocol visibility groups
distinct.
Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3179)
Emit every comma-separated property/ivar declarator, and count @interface
after a multiline block comment closes so in-declaration macros stay intact.
Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: ximengkai <ximengkai@soyoung.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>