GH-46646: [dev][R] Replace linr with jarl for R linting / pre-commit check
Project: apache/arrow | Language: C++ | Concepts: configuration-management, testing-strategy, dependency-management | Difficulty: mid
Link: https://github.com/apache/arrow/pull/50851
The problem
The project used the lintr R package for linting R code in pre-commit checks. This required a working R installation, which caused failures in environments without R (or with non-standard setups like conda). This made the developer and CI experience unreliable and brittle.
Why it's non-trivial
Simply removing or replacing the linter isn't enough:
- The replacement must work cross-platform, without requiring R.
- The new linter must be integrated into pre-commit and CI.
- Generated files and known problematic rules must be excluded to avoid false positives and CI failures.
- Documentation and developer workflow instructions need updating.
The approach
1. Remove R dependency from CI and pre-commit cache:
- r-base \
Removes the installation of R from the CI workflow, since it's no longer needed for linting.
- ~/.local/share/renv/cache
Removes the R package cache from pre-commit caching, further decoupling from R.
2. Replace the lintr pre-commit hook with jarl:
- - repo: local
+ - repo: https://github.com/etiennebacher/jarl-pre-commit
+ rev: "0.5.0"
hooks:
- - id: lintr
+ - id: jarl-check
alias: r
- name: R Lint
- language: r
- additional_dependencies:
- - cyclocomp
- - lintr
- - testthat
- entry: |
- Rscript -e "Sys.setenv(NOT_CRAN = 'TRUE'); lintr::expect_lint_free('r')"
- pass_filenames: false
- files: >-
- ^r/.*\.(R|Rmd)$
+ name: R Lint (jarl)
Swaps out the local lintr hook (which required R and extra dependencies) for the Rust-native, standalone jarl linter. This means linting no longer depends on R being installed.
3. Add a jarl configuration file to control linting behavior:
# r/jarl.toml
[lint]
exclude = ["R/arrowExports.R", "R/dplyr-funcs-doc.R", "data-raw/codegen.R"]
ignore = [
"any_is_na",
"comparison_negation",
"download_file",
"duplicated_arguments",
"equals_null",
"implicit_assignment",
"internal_function",
"numeric_leading_zero",
"outer_negation",
"redundant_equals",
"redundant_ifelse",
"repeat",
"seq",
"sprintf",
"unused_function",
]
Defines which files to exclude (generated or codegen files) and which rules to ignore (existing violations that would otherwise fail CI). This ensures a smooth transition and green CI.
4. Update developer documentation and workflow instructions:
-* [`lintr`](https://github.com/r-lib/lintr) for code analysis
+* [`jarl`](https://etiennebacher.github.io/jarl/) for code analysis
Replaces references to lintr with jarl in documentation and vignettes.
-```r
-lintr::lint_package()
-```
+```
+jarl check .
+```
Updates usage instructions so developers know how to run the new linter.
From the review thread
Copilot:
jarl.tomlis added underr/, but the pre-commit hook runs from the repo root. Per Jarl’s config discovery, it only searchesjarl.tomlin the current working directory and parent directories, sor/jarl.tomlwon’t be picked up; the excludes there won’t apply and generated files may get linted unexpectedly. Consider either movingjarl.tomlto the repo root, or changing the hook to execute withcwd=r/(bearing in mind Windows support / filename path rewriting).
jorisvandenbossche:
The pre-commit logs in the failing CI clearly indicates
Used '/home/runner/work/arrow/arrow/r/jarl.toml', so it seems the config file is correctly used
Takeaways
- Prefer standalone, language-agnostic tools for CI checks to avoid fragile system dependencies.
- When swapping out a linter or formatter, update all related configuration, documentation, and ignore lists to ensure a smooth transition.
- Always verify how configuration files are discovered and loaded in your toolchain, especially when changing directory structures or tool invocation paths.