Skip to content

Commit cb8636a

Browse files
Merge branch 'main' into default-pipe
2 parents 1ba785e + 0939d20 commit cb8636a

6 files changed

+124
-10
lines changed

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
+ `linter=` argument of `Lint()`.
1111
+ `with_defaults()`.
1212
+ Linters `closed_curly_linter()`, `open_curly_linter()`, `paren_brace_linter()`, and `semicolon_terminator_linter()`.
13+
* Argument `interpret_glue` to `object_usage_linter()` is deprecated in favor of the more general `interpret_extensions`, in which `"glue"` is present by default (#1472, @MichaelChirico). See the description below.
1314
* The default for `pipe_consistency_linter()` is changed from `"auto"` (require one pipe style, either magrittr or native) to `"|>"` (R native pipe required) to coincide with the same change in the Tidyverse Style Guide (#2707, @MichaelChirico).
1415

1516
## Bug fixes
@@ -28,6 +29,7 @@
2829
* `indentation_linter()` handles `for` un-braced for loops correctly (#2564, @MichaelChirico).
2930
* Setting `exclusions` supports globs like `knitr*` to exclude files/directories with a pattern (#1554, @MichaelChirico).
3031
* `object_name_linter()` and `object_length_linter()` apply to objects assigned with `assign()` or generics created with `setGeneric()` (#1665, @MichaelChirico).
32+
* `object_usage_linter()` gains argument `interpret_extensions` to govern which false positive-prone common syntaxes should be checked for used objects (#1472, @MichaelChirico). Currently `"glue"` (renamed from earlier argument `interpret_glue`) and `"rlang"` are supported. The latter newly covers usage of the `.env` pronoun like `.env$key`, where `key` was previously missed as being a used variable.
3133

3234
### Lint accuracy fixes: removing false positives
3335

R/object_usage_linter.R

+54-4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
#' Check that closures have the proper usage using [codetools::checkUsage()].
44
#' Note that this runs [base::eval()] on the code, so **do not use with untrusted code**.
55
#'
6-
#' @param interpret_glue If `TRUE`, interpret [glue::glue()] calls to avoid false positives caused by local variables
7-
#' which are only used in a glue expression.
6+
#' @param interpret_glue (Deprecated) If `TRUE`, interpret [glue::glue()] calls to avoid
7+
#' false positives caused by local variables which are only used in a glue expression.
8+
#' Provide `interpret_extensions` instead, see below.
9+
#' @param interpret_extensions Character vector of extensions to interpret. These are meant to cover known cases where
10+
#' variables may be used in ways understood by the reader but not by `checkUsage()` to avoid false positives.
11+
#' Currently `"glue"` and `"rlang"` are supported, both of which are in the default.
12+
#' - For `glue`, examine [glue::glue()] calls.
13+
#' - For `rlang`, examine `.env$key` usages.
814
#' @param skip_with A logical. If `TRUE` (default), code in `with()` expressions
915
#' will be skipped. This argument will be passed to `skipWith` argument of
1016
#' `codetools::checkUsage()`.
@@ -29,7 +35,27 @@
2935
#' @evalRd rd_linters("package_development")
3036
#' @seealso [linters] for a complete list of linters available in lintr.
3137
#' @export
32-
object_usage_linter <- function(interpret_glue = TRUE, skip_with = TRUE) {
38+
object_usage_linter <- function(interpret_glue = NULL, interpret_extensions = c("glue", "rlang"), skip_with = TRUE) {
39+
if (!is.null(interpret_glue)) {
40+
lintr_deprecated(
41+
"interpret_glue",
42+
'"glue" in interpret_extensions',
43+
version = "3.3.0",
44+
type = "Argument",
45+
signal = "warning"
46+
)
47+
48+
if (interpret_glue) {
49+
interpret_extensions <- union(interpret_extensions, "glue")
50+
} else {
51+
interpret_extensions <- setdiff(interpret_extensions, "glue")
52+
}
53+
}
54+
55+
if (length(interpret_extensions) > 0L) {
56+
interpret_extensions <- match.arg(interpret_extensions, several.ok = TRUE)
57+
}
58+
3359
# NB: difference across R versions in how EQ_ASSIGN is represented in the AST
3460
# (under <expr_or_assign_or_help> or <equal_assign>)
3561
# NB: the repeated expr[2][FUNCTION] XPath has no performance impact, so the different direct assignment XPaths are
@@ -77,7 +103,7 @@ object_usage_linter <- function(interpret_glue = TRUE, skip_with = TRUE) {
77103
if (inherits(fun, "try-error")) {
78104
return()
79105
}
80-
known_used_symbols <- extract_glued_symbols(fun_assignment, interpret_glue = interpret_glue)
106+
known_used_symbols <- known_used_symbols(fun_assignment, interpret_extensions = interpret_extensions)
81107
res <- parse_check_usage(
82108
fun,
83109
known_used_symbols = known_used_symbols,
@@ -261,3 +287,27 @@ get_imported_symbols <- function(xml) {
261287
)
262288
}))
263289
}
290+
291+
known_used_symbols <- function(fun_assignment, interpret_extensions) {
292+
unique(c(
293+
if ("rlang" %in% interpret_extensions) extract_env_symbols(fun_assignment),
294+
if ("glue" %in% interpret_extensions) extract_glued_symbols(fun_assignment)
295+
))
296+
}
297+
298+
extract_env_symbols <- function(fun_assignment) {
299+
env_names <- xml_find_all(
300+
fun_assignment,
301+
"
302+
.//SYMBOL[text() = '.env']
303+
/parent::expr
304+
/following-sibling::OP-DOLLAR
305+
/following-sibling::SYMBOL
306+
| .//SYMBOL[text() = '.env']
307+
/parent::expr
308+
/following-sibling::LBB
309+
/following-sibling::expr[STR_CONST]
310+
"
311+
)
312+
get_r_string(env_names)
313+
}

R/shared_constants.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ strip_names <- function(x) {
262262
#' @return A character vector of symbols (variables, infix operators, and
263263
#' function calls) found in glue calls under `expr`.
264264
#' @noRd
265-
extract_glued_symbols <- function(expr, interpret_glue) {
265+
extract_glued_symbols <- function(expr, interpret_glue = TRUE) {
266266
if (!isTRUE(interpret_glue)) {
267267
return(character())
268268
}

R/unused_import_linter.R

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#' Check that imported packages are actually used
22
#'
3-
#' @inheritParams object_usage_linter
3+
#' @param interpret_glue If `TRUE`, interpret [glue::glue()] calls to avoid false positives caused by local variables
4+
#' which are only used in a glue expression.
45
#' @param allow_ns_usage Suppress lints for packages only used via namespace.
56
#' This is `FALSE` by default because `pkg::fun()` doesn't require `library(pkg)`.
67
#' You can use [requireNamespace("pkg")][requireNamespace()] to ensure a package is

man/object_usage_linter.Rd

+16-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-object_usage_linter.R

+49-1
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ test_that("interprets glue expressions", {
418418
local_var <- 42
419419
glue::glue('The answer is {local_var}.')
420420
}
421-
"), "local_var", object_usage_linter(interpret_glue = FALSE))
421+
"), "local_var", object_usage_linter(interpret_extensions = NULL))
422422

423423
# call in glue is caught
424424
expect_lint(
@@ -858,3 +858,51 @@ test_that("globals in scripts are found regardless of assignment operator", {
858858
linter
859859
)
860860
})
861+
862+
test_that("dplyr's .env-specified objects are marked as 'used'", {
863+
skip_if_not_installed("dplyr")
864+
skip_if_not_installed("rlang")
865+
linter <- object_usage_linter()
866+
867+
expect_lint(
868+
trim_some("
869+
foo <- function(df) {
870+
source <- 1
871+
target <- 2
872+
unused <- 3
873+
874+
df %>%
875+
dplyr::mutate(
876+
from = rlang::.env$source,
877+
to = rlang::.env[['target']]
878+
)
879+
}
880+
"),
881+
rex::rex("local variable", anything, "unused"),
882+
linter
883+
)
884+
})
885+
886+
test_that("interpret_glue is deprecated", {
887+
expect_warning(
888+
{
889+
linter_no <- object_usage_linter(interpret_glue = FALSE)
890+
},
891+
rex::rex("interpret_glue", anything, "deprecated")
892+
)
893+
expect_warning(
894+
{
895+
linter_yes <- object_usage_linter(interpret_glue = TRUE)
896+
},
897+
rex::rex("interpret_glue", anything, "deprecated")
898+
)
899+
900+
code <- trim_some("
901+
fun <- function() {
902+
local_var <- 42
903+
glue::glue('The answer is {local_var}.')
904+
}
905+
")
906+
expect_lint(code, "local_var", linter_no)
907+
expect_no_lint(code, linter_yes)
908+
})

0 commit comments

Comments
 (0)