Jarl 0.6.0
I’m glad to announce the release of Jarl 0.6.0. Jarl is a very fast R linter, written in Rust. It finds inefficient, hard-to-read, and suspicious patterns of R code across dozens of files and thousands of lines of code in milliseconds. Install or update Jarl via the command line or install the Jarl extension in Positron, VS Code, Zed, and more.
A quick summary before diving into the details:
- Jarl has a new experimental rule (
unused_object) to detect objects that are defined but never used. This is disabled by default because there may be false positives. However, we released it both because it can already be very useful and because it can be improved thanks to your bug reports! - 16 new rules (in addition to
unused_object) - better experience with the command-line interface (CLI)
- Jarl can be installed via
uv,pixi,mise, and more!
As usual, this release also comes with other small features and a whole lot of bug fixes. You can find the full list of changes in the changelog.
Find unused objects
Jarl 0.6.0 brings many new rules but unused_object probably is the most significant, and definitely was the most complicated to implement. unused_object is a rule that finds objects that are defined but never used, meaning that these definitions can be removed without affecting the rest of the code. Unused objects are usually pieces of code that were forgotten while refactoring some code, but they can also signal a bug somewhere if we expected this object to be used.
This rule is hard to implement for two reasons.
Semantic analysis
The first one is shared across all languages: we need to have a good representation of the meaning of the code.
So far, most rules rely on patterns. For example, if we find a piece of code that follows the pattern any(is.na(<other code>)), then we can report a violation of the any_is_na rule. This is relatively easy to implement because this is a small pattern that follows a logical construct: if we find a call to any() containing a call to is.na(), we report it.
In the case of unused_object, we need to find whether an object that was just defined is used anywhere in the subsequent code, whether it is called in a different file, whether it is overwritten before actually being used, etc.
We need to be able to analyze the relationship of all R objects in our code. In other words, we need to do some semantic analysis.
This is a big challenge that I haven’t tackled myself. Instead, I relied upon the work of Lionel Henry and Davis Vaughan in Ark (in particular the Oak project). With this, Jarl can now read the entire semantic structure of R code, which also requires handling the case of multi-file projects that share objects, such as R packages or scripts that call source(). Big thanks to both of them for their work!
Everyone can manipulate R
We have a strong foundation for our semantic analysis, and this already covers a lot of cases in real-world projects. Now comes the second reason why this rule is hard to implement: anyone can manipulate R code, and this leads to cases that slip through the cracks:
string interpolation:
x <- 1 glue::glue("{x}")1Jarl automatically handles string interpolation in the
glue,cli, andstringrpackages, but anyone could create their own functions to do string interpolation (see for instance stringmagic) and Jarl isn’t capable of handling them.metaprogramming:
x <- 1 q <- quote(x) env <- environment() eval(q, envir = env) #> [1] 1 env[["x"]] <- 2 eval(q, envir = env) #> [1] 2Suppose we cannot run any code (as is the case in a static analysis tool like Jarl), should we consider that our
x <- 1definition above is used? Hard to say because it entirely depends onenv, which may have been modified in another place, and we can’t explore its contents. Currently, Jarl reportsxas unused.Similarly, in the following code, can we detect that
xis actually used?my_expr <- quote(x + 1) f <- function(expr) { x <- 1 eval(expr) } f(my_expr)[1] 2When we evaluate the code, we can see that it is used, but in static analysis this is much harder to do because once we are in the function body, we would need to guess that
xmight be used ineval(), walk back toexpr, leading tomy_expr, detect thatquote()is used and thatxis part of it.This might be doable in this simple example, but it is much harder to generalize. Currently, Jarl reports
xas unused.
Nevertheless, despite these flaws, I believe that unused_object will be very useful in many projects.
Reminder: for now, you need to explicitly opt-in to use this rule.
New rules
16 new rules have been added since 0.5.0 (in addition to unused_object), thanks to several contributors. Most of these rules also exist in lintr, meaning that Jarl slowly but surely gets closer to feature parity, but a few of them are not found there. In particular, Jarl now detects possible mistakes in glue::glue(), such as calls without any string interpolation:
warning: glue
--> foo.R:1:1
|
1 | glue('{a}', .open = '<', .close = '>')
| -------------------------------------- This `glue()`
| call isn't necessary because it performs no
| interpolation.
|
Better CLI experience
The CLI received some small but useful improvements.
Autocomplete suggestions
It is now possible to press <TAB> to have suggestions of commands or rule names accepted by Jarl. For instance, jarl check . --select any<TAB> would suggest either any_is_na or any_duplicated.
This is supported in several shells, such as fish, zsh, and bash. See the documentation to know how to set this up with your shell.
jarl rule
Jarl now has a new command jarl rule <rulename> to print the documentation of a specific rule directly in the terminal, hence avoiding an extra trip to the website.
> jarl rule any_is_na
any_is_na
Categories: PERF
Enabled by default: yes
Fix: safe
Added in 0.0.8
## What it does
Checks for usage of `any(is.na(...))`, `NA %in% x`, and `NA %notin% x`.
[...]
Rule name suggestions
Jarl now suggests similar rule names when there is a typo in one of the names:
> jarl check . --select duplicated_argument,any_i_na
jarl failed
Cause: Unknown rules in `--select`: duplicated_argument, any_i_na
Help: Did you mean "duplicated_arguments"?
Help: Did you mean "any_is_na"?
Exclude folders
Jarl can exclude folders with the --exclude argument, similar to the exclude argument in jarl.toml. Note that this argument must take a =:
> jarl check . --exclude=inst,tests
Better help page organization
The help page is now clearly split into various sections:
Check a set of files or directories
Usage: jarl check [OPTIONS] <FILES>...
Arguments:
<FILES>...
List of files or directories to check or fix lints, for example `jarl check .`.
File selection:
--exclude <FILES>
List of file patterns to exclude from linting, separated by a comma (no spaces).
[TRUNCATED FOR CONCISENESS]
Rule selection:
-s, --select <RULES>
Names of rules to include, separated by a comma (no spaces). This also accepts names of groups of rules, such as "PERF".
[default: ""]
[TRUNCATED FOR CONCISENESS]
Fix options:
-f, --fix
Automatically fix issues detected by the linter.
[TRUNCATED FOR CONCISENESS]
Other options:
-w, --with-timing
Show the time taken by the function.
[TRUNCATED FOR CONCISENESS]
Global options:
--log-level <LOG_LEVEL>
The log level. One of: `error`, `warn`, `info`, `debug`, or `trace`. Defaults to `warn`
New ways to install Jarl
Jarl is now available on PyPI and conda-forge, meaning that it can be installed by uv, mise, pixi, and potentially more tools.
It can also be installed via Homebrew. See the installation instructions for more info.
Conclusion
Jarl 0.6.0 brings many exciting features, try them out! If you find any issue, have feature ideas, or want to contribute, head to the Github repository.
I’m glad that this release got code contributions from seven people (in addition to myself), and I want to thank them and everyone else who contributed one way or another: @atsyplenkov, @Bisaloo, @christopherkenny, @dieghernan, @fh-mthomson, @gisler, @Goldziher, @hfrick, @ilyaZar, @JosephBARBIERDARNAL, @JosiahParry, @lwjohnst86, @maelle, @mattkerlogue, @njtierney, @novica, @playmobilmeister, @randy3k, @sebffischer, @snystrom, and @Yousa-Mirage.