0.6.0 - 2026-07-17
Release Notes
Driven by the ry 0.5.0 top-500 CRAN audit (9,237 diagnostics, 1.55%
precision) and a subsequent generalization pass. On the same 504-package
corpus this release emits 3,442 diagnostics (-63%; -69% counting only
warnings/errors), with every previously cataloged true positive either
preserved or its loss individually adjudicated, and ~10 new real shipped
bugs found by the new RY100 rule. Minor bump: scope resolution, rule
routing (RY001/RY003), and quoting semantics intentionally change
reported diagnostics.
Added
- RY003
numeric-condition(Info): numericif/whileconditions are
legal, idiomatic R (if (nchar(x)),if (n)); they are no longer
RY001 warnings. RY001 keeps the genuinely erroneous modes (character,
list, NULL, function, length-0). - RY100
comparison-inside-math-call(Warning): a comparison directly
insideabs/sqrt/exp/log*/floor/ceiling/round/truncis
almost always a parenthesization slip (abs(x > y)forabs(x) > y).
Generalizes RY093, ry's highest-precision rule; corpus census found 10+
real shipped bugs (effects, ggplot2 tests, performance, pracma) at ~100%
precision after excluding the deliberatesign(cmp)indicator idiom. - RY040 fires on arithmetic with a known-NULL operand (
x / NULLis
numeric(0)), gated to literal NULLs and missing fields of complete,
locally builtlist(...)schemas so parameter defaults never trip it. - Environment profiles: files sourced into a known framework context get
its ambient bindings. Shiny app trees (input/output/session) ship
built in; users declare their own via[[environments]]inry.toml
(name,bindings,paths). ry.tomlmax-serialized-bytes(default 2 MiB) caps.rdaworkspace
enumeration; oversized workspaces open the file's scope instead of
stalling the scan (bigD: 190 s -> 0.13 s).- File collection accepts the full R source extension set (
.S,.s,
.q— boot's entire library was previously invisible), decodes Latin-1
sources instead of skipping them, and skips*.Rcheckbuild artifacts.
Scope and name resolution
library()/require()of a package without a stub marks the search
path unknown, silencing RY010 for names that plausibly come from it —
the single largest false-positive source in the audit (lazy-loaded
datasets such assleepstudy,apipop). Stubbed packages keep full
checking.data()/load()/source()/sys.source()declare the same
effect via stub metadata;data(x)also binds its literal names.- Attachment is context-scoped to match R's semantics: package
R/code
resolves bare names against base plus exactly what NAMESPACE grants
(importFromnames, wholesaleimport(pkg)exports); test and script
files resolve against the testthat runner world (testthat, the package
under test, helper/setup and in-filelibrary()calls, and DESCRIPTION
Suggests). Imports no longer leak whole-package exports into files that
never attached them (arrow'sstring/int/dblvs rlang). - Loop bodies pre-bind names assigned anywhere in the body, so
loop-carried accumulators read before their first syntactic assignment
no longer fire RY010. on.exit(expr)is checked against exit-time bindings (everything the
function assigns), not walk-order bindings.
NSE and quoting
- User functions that quote their arguments are detected from their
bodies —substitute/match.call/sys.call/bquoteand, via stub
metadata, the rlang capture family (enquo,enexpr,ensym, plural
forms,quos) — and the property propagates: through direct argument
forwarding between user functions, from stub eval modes into user
wrappers, and from S3 methods onto their generics (named method params
absorbed by the generic's...included). lambda.r: 165 -> 0 RY010;
sparklyr: 93 -> 0. - Quoted arguments receive no diagnostics at all — they are data, not
code (igraph'sgraph_from_literal(A +-+ B)no longer type-errors). - Operands of unknown
%op%infix operators and unresolvable.()
calls are treated as quoted. - Formula-interface arguments (
weights,subset,offset,id,
cluster,istate) evaluate inside thedatamask via the new
data_mask_sourcestub metadata (stats and survival interfaces). - String-literal calls (
"paste"(1, 2),"[<-.data.frame"(...)) resolve
like identifiers instead of firing RY070; character variables in call
position still do.
Type system
- Divergence-aware narrowing: a guard whose branch always exits
(if (is.null(x)) stop(...),return,abortvia the newno_return
stub property,if (!length(x)) return(...)) narrows the continuation.
Never-returning user helpers are detected recursively; a project-local
function namedabortis not assumed to diverge. - Narrowing-installed bindings are tracked explicitly, so a real
assignment inside a branch always overrides a temporary refinement in
the post-if merge (fixes stale-NULL cascades through the cross-file
fixpoint). df[, j]single-column selection honorsdrop = TRUE(a parser fix:
the empty row index was previously dropped entirely) and returns the
column type; scalar subscripts narrow to length 1; negative literals
keep vector length.- S3 dispatch walks the full class vector across all method sources;
Ops/Math/Summarygroup generics dispatch for data.frames and user
classes (df / 2,ggplot() + NULL-style idioms); RY050 fires only
for generics the project itself demonstrably owns. list(...)containing dots yields an incomplete schema — a missing
field is no longer known-NULL;$/[[through a parameter whose only
evidence is an overridable NULL default yields unknown.- A condition typed as a union with at least one valid length-1 logical
member is not reported (only provably invalid unions are). append()returns the concatenation of its arguments;tapplygained
a higher-order simplify spec;mapplyhonorsSIMPLIFY = TRUE
(all stub-data fixes, vendored from r-typeshed 0.2.0 along with new
rlang and cli stubs).
Fixed
- Panic (
index out of bounds) in quoting-forwarding when a user callee
and a stub callee had different parameter counts; it crashed scans of
17 corpus packages (psych, rlang, recipes, …). readLines()no longer demandscon(stub had it wrongly required);
a generator-level fix detectsmissing()-based optionality so the
whole class (rlang::env_get(default=)) cannot recur.- RY033's stale-type false positives after both
if/elsearms rebind a
variable. - RY100 subsumes the condition-type diagnostic on the same span (no
double reporting).
Driven by the ranks-301-500 audit (ry 0.4.0 on the top-500 CRAN packages).
Minor bump: RY050's dispatch semantics, RY097's collapse criteria, and the
new binding/quoting/narrowing rules intentionally change reported
diagnostics between versions.
Performance
- Pipe-chain inference was exponential: each
%>%/|>stage re-inferred
its entire left-hand side inside the desugared call, so a 20-stage chain
took ~14 s and longer chains never finished. The inferred LHS type is
now reused. gt (289 R files, previously unscannable) checks in ~2.4 s. - The required-parameter force-flow analysis walked each
ifbranch twice
(once for "forces", once for "falls through"), which is exponential on
longelse ifdispatcher chains. Both facts are now computed in one
pass. lavaan and stargazer (previously >60 min, never completed) check
in ~2.3 s and ~0.8 s.
Fixed
assign("name", value, envir = ...),makeActiveBinding(), and
delayedAssign()with a literal name now create package-level bindings
(any nesting depth underR/). Removes whole-package RY010 cascades in
clock (204 -> 0), rJava, otel, parallelly, and others..packageName
is bound in every package namespace.- A string-literal assignment target (
"Math.foo" <- function(...)) now
binds, aliases, and establishes S3 dispatch context (.Generic,
.Method) exactly like an identifier target (chron 35 -> 10). alist()arguments are quoted, never resolved as variables, and the
call returns a list (Deriv 111 -> 8, ade4 RY010 42 -> 2).- A union whose members are all functions is callable; RY070 no longer
fires onf <- if (p) function(...) ... else function(...)followed by
f(...). Argument checks report only findings that hold for every
member signature. NULL/function unions still report RY070. - RY097 (not-R-source) now also collapses files that mostly parse as R
but are riddled with parse errors (>= 5 errors and >= 15% of top-level
statements): Ratfor, GAUSS, and markdown-table files underinst/
(pacman 270 -> 27, plm 136 -> 36 total). is.list()/is.function()/is.environment()/is.data.frame()guards
narrow a parameter whose type came only from its default, so
f <- function(x = FALSE) if (is.list(x)) x$fieldno longer reports
RY061 (visNetwork 34 -> 5 RY061).- Assignments nested inside call arguments of
if/whileconditions
(if (grepl(p, ti <- text[i]))) now bind in the enclosing scope
(litedown 27 -> 10). - RY033's message no longer claims R compares "byte values"; R coerces
the numeric operand to character and compares lexicographically. - The typeshed ships registered-but-unexported base S3 methods (e.g.
stats:::print.anova), so RY050 no longer reports them missing
(spatial, Cairo). RY050 also honors<generic>.defaultas a valid
dispatch fallback:coef(glm_fit)no longer reports a missing
coef.glm. Consequently RY050 can no longer fire for generics that
have a.defaultmethod (such asprint) — dispatch always succeeds
for them.
Removed
RY095(negation-comparison-precedence) is retired. The rule assumed C
operator precedence, but R gives unary!lower precedence than
comparison operators:!x == yparses as!(x == y), so every flagged
site was correct code and the suggested rewrite was a semantic no-op.
The rule number will not be reused.
Fixed
RY096no longer fires in functions whose formals include...:
there,hasArg(name)legitimately tests for a dots-supplied argument
(if (hasArg(b)) list(...)$b). All 84 corpus hits were this idiom.
The rule now only flags the provable case — ahasArg()naming a
non-formal in a function without...is alwaysFALSE.
Corrections to 0.4.0 release notes
- The scales
!length(x) == 1guards cited as newly found bugs were not
bugs; they parse aslength(x) != 1and behave as intended. The same
applies to RY095 reports in rpart, mice, quantreg, spdep, and mlflow.
Install ry-cli 0.6.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/sims1253/ry/releases/download/v0.6.0/ry-cli-installer.sh | shInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/sims1253/ry/releases/download/v0.6.0/ry-cli-installer.ps1 | iex"Download ry-cli 0.6.0
| File | Platform | Checksum |
|---|---|---|
| ry-cli-aarch64-apple-darwin.tar.gz | Apple Silicon macOS | checksum |
| ry-cli-x86_64-apple-darwin.tar.gz | Intel macOS | checksum |
| ry-cli-aarch64-pc-windows-msvc.zip | ARM64 Windows | checksum |
| ry-cli-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| ry-cli-aarch64-unknown-linux-gnu.tar.gz | ARM64 Linux | checksum |
| ry-cli-x86_64-unknown-linux-gnu.tar.gz | x64 Linux | checksum |