Skip to content

0.6.0 - 2026-07-17

Choose a tag to compare

@github-actions github-actions released this 17 Jul 16:20
426e78b

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): numeric if/while conditions 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
    inside abs/sqrt/exp/log*/floor/ceiling/round/trunc is
    almost always a parenthesization slip (abs(x > y) for abs(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 deliberate sign(cmp) indicator idiom.
  • RY040 fires on arithmetic with a known-NULL operand (x / NULL is
    numeric(0)), gated to literal NULLs and missing fields of complete,
    locally built list(...) 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]] in ry.toml
    (name, bindings, paths).
  • ry.toml max-serialized-bytes (default 2 MiB) caps .rda workspace
    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 *.Rcheck build 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 as sleepstudy, 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
    (importFrom names, wholesale import(pkg) exports); test and script
    files resolve against the testthat runner world (testthat, the package
    under test, helper/setup and in-file library() calls, and DESCRIPTION
    Suggests). Imports no longer leak whole-package exports into files that
    never attached them (arrow's string/int/dbl vs 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/bquote and, 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's graph_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 the data mask via the new
    data_mask_source stub 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, abort via the new no_return
    stub property, if (!length(x)) return(...)) narrows the continuation.
    Never-returning user helpers are detected recursively; a project-local
    function named abort is 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 honors drop = 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/Summary group 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; tapply gained
    a higher-order simplify spec; mapply honors SIMPLIFY = 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 demands con (stub had it wrongly required);
    a generator-level fix detects missing()-based optionality so the
    whole class (rlang::env_get(default=)) cannot recur.
  • RY033's stale-type false positives after both if/else arms 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 if branch twice
    (once for "forces", once for "falls through"), which is exponential on
    long else if dispatcher 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 under R/). 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 on f <- 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 under inst/
    (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$field no longer reports
    RY061 (visNetwork 34 -> 5 RY061).
  • Assignments nested inside call arguments of if/while conditions
    (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>.default as 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 .default method (such as print) — 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 == y parses 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

  • RY096 no 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 — a hasArg() naming a
    non-formal in a function without ... is always FALSE.

Corrections to 0.4.0 release notes

  • The scales !length(x) == 1 guards cited as newly found bugs were not
    bugs; they parse as length(x) != 1 and 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 | sh

Install 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