Static Analysis in R and for Bioconductor

Bioconductor TAB meeting 02/2026

Hugo Gruson

February 5, 2026

Definition

Static code analysis and linting refer to the concept of detecting incorrect, suboptimal or dangerous code patterns without executing the code, only by analysing the code syntax tree.

Rationale

Static analysis:

  • may catch bugs early
  • may improve code readability, maintainability, performance

Benefits over testing

  • fast: even for packages doing heavy computations
  • safe: untrusted, potentially unsafe code, doesn’t need to be executed
  • robust: non-reproducible code or incomplete code (e.g., missing data) can be analyzed as long as it’s valid code.

Static analysis frameworks in the R ecosystem

Tool / framework Runs in Bioconductor Runs in CRAN Runs in rOpenSci
R CMD check
biocCheck
rchk
lintr ( / flir / jarl ) (✔ *)

*: during initial onboarding only

biocCheck

Focused on Bioconductor best practices.

Well-known to this group.

lintr

Not just style!

 [1] "best_practices"      "common_mistakes"     "configurable"       
 [4] "consistency"         "correctness"         "default"            
 [7] "efficiency"          "executing"           "package_development"
[10] "pkg_testthat"        "readability"         "regex"              
[13] "robustness"          "style"               "tidy_design"        
lintr::lint(
  text = "length(x == 0)",
  linters = lintr::length_test_linter()
)
::warning file=<text>,line=1,col=1::file=<text>,line=1,col=1,[length_test_linter] Checking the length of a logical vector is likely a mistake. Did you mean `length(x) == 0`?
lintr::lint(
  text = "x == NA",
  linters = lintr::equals_na_linter()
)
::warning file=<text>,line=1,col=1::file=<text>,line=1,col=1,[equals_na_linter] Use is.na() instead of x == NA

lintr vs biocCheck

biocCheck check linter
vapply() instead of sapply() undesirable_function_linter()
seq_len() or seq_along() instead of 1:length() seq_linter()
pkg:fun() instead of pkg::fun() No
Download links to GitHub/GitLab/BitBucket No
download.file() in onLoad() No
paste() in condition messages condition_message_linter()
message() instead of cat()/print() print_linter()
<- instead of = assignment_linter()
No .Deprecated/.Defunct/etc. No
TRUE/FALSE instead of T/F T_and_F_symbol_linter()
is() instead of class() == class_equals_linter()
sytem2() instead of system() No
No set.seed() No
No direct slot access (@) No
No browser() undesirable_function_linter()
No <<- undesirable_operator_linter()
No Sys.setenv() undesirable_function_linter()
No suppressWarnings() No

All missing linters could very easily be implemented!

lintr vs biocCheck proposal

  • Opportunity: Cross-pollination between biocCheck and lintr:
    • reduce maintenance burden
    • increase user base (= more test cases, more feedback)
    • make Bioconductor checks available to the wider R community
  • Proposal: bioconductor category
lintr::lint_package(linters = lintr::linters_with_tags("bioconductor"))

R CMD check vs biocCheck

Some shared checks, such as:

class(x) == "myClass"

Not necessarily an issue(?)

rchk: static analysis for C code in R packages

PROTECT() / UNPROTECT() balance and common mistakes in C code interacting with the R C API.

Additional resources:

rchk in CRAN & Bioconductor

Example in a recent submission:

  > library(testthat)
  > library(redacted)
  > 
  > test_check("redacted")
  Warning: stack imbalance in '::', 79 then 81
  Warning: stack imbalance in '<-', 77 then 79
  Warning: stack imbalance in '{', 73 then 75
  Saving _problems/test-PlotExpression-83.R
  [ FAIL 1 | WARN 0 | SKIP 0 | PASS 108 ]

Value of rchk for Bioconductor

Value for our community:

  • Available to individual developers as a Docker image or as a GitHub Action via R-hub
    • May be hard to set up
    • Upstream Docker image not compatible with latest Bioconductor releases
  • Bioconductor has a lot of native C code

Proposal 1: new checks on Bioconductor

Tool / framework Runs in Bioconductor Runs in CRAN Runs in rOpenSci
R CMD check
biocCheck
rchk 💡
lintr ( / flir / jarl ) 💡 (via biocCheck) (✔ *)

New runners for checks could be hosted at EMBL, since:

  • proposed checks are entirely independent from BBS
  • we already have a full copy of the sources

Proposal 2: integration of lintr in biocCheck

Share some code components between biocCheck and lintr:

  • reduce maintenance burden
  • increase user base (= more test cases, more feedback)
  • make Bioconductor checks available to the wider R community

Discussion