Skip to content

Commit 1e08b13

Browse files
Make object_usage_linter glue aware (#969)
* fix #942 * add test for .open and .close * add test for non-default argument * [temp] disable tryCatch() to check build system check failures * Revert "[temp] disable tryCatch() to check build system check failures" This reverts commit f3f2335. * document() * use keep.source = TRUE * remove nolints, add TODO comment * comments Co-authored-by: Michael Chirico <chiricom@google.com>
1 parent 9e3ddab commit 1e08b13

File tree

7 files changed

+138
-10
lines changed

7 files changed

+138
-10
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ function calls. (#850, #851, @renkun-ken)
100100
* `assignment_linter()` now lints right assignment (`->` and `->>`) and gains two arguments. `allow_cascading_assign` (`TRUE` by default) toggles whether to lint `<<-` and `->>`; `allow_right_assign` toggles whether to lint `->` and `->>` (#915, @michaelchirico)
101101
* `infix_spaces_linter()` gains argument `exclude_operators` to disable lints on selected infix operators. By default, all "low-precedence" operators throw lints; see `?infix_spaces_linter` for an enumeration of these. (#914 @michaelchirico)
102102
* `infix_spaces_linter()` now throws a lint on `a~b` and `function(a=1) {}` (#930, @michaelchirico)
103+
* `object_usage_linter()` now detects usages inside `glue::glue()` constructs (#942, @AshesITR)
103104

104105
# lintr 2.0.1
105106

R/expect_comparison_linter.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ expect_comparison_linter <- function() {
1818
xml <- source_file$xml_parsed_content
1919

2020
# != doesn't have a clean replacement
21-
comparator_nodes <- # nolint: object_usage_linter. TODO(#942): remove this.
22-
setdiff(as.list(infix_metadata$xml_tag[infix_metadata$comparator]), "NE")
21+
comparator_nodes <- setdiff(as.list(infix_metadata$xml_tag[infix_metadata$comparator]), "NE")
2322
xpath <- glue::glue("//expr[
2423
expr[SYMBOL_FUNCTION_CALL[text() = 'expect_true']]
2524
and expr[2][ {xp_or(comparator_nodes)} ]

R/expect_s3_class_linter.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ expect_s3_class_linter <- function() {
1818

1919
# (1) expect_{equal,identical}(class(x), C)
2020
# (2) expect_true(is.<class>(x)) and expect_true(inherits(x, C))
21-
is_class_call <- xp_text_in_table(c(is_s3_class_calls, "inherits")) # nolint: object_usage_linter.
21+
is_class_call <- xp_text_in_table(c(is_s3_class_calls, "inherits"))
2222
xpath <- glue::glue("//expr[
2323
(
2424
SYMBOL_FUNCTION_CALL[text() = 'expect_equal' or text() = 'expect_identical']

R/expect_type_linter.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ expect_type_linter <- function() {
1616

1717
xml <- source_file$xml_parsed_content
1818

19-
base_type_tests <- xp_text_in_table(paste0("is.", base_types)) # nolint: object_usage_linter. TODO(#942): fix this.
19+
base_type_tests <- xp_text_in_table(paste0("is.", base_types))
2020
xpath <- glue::glue("//expr[
2121
(
2222
SYMBOL_FUNCTION_CALL[text() = 'expect_equal' or text() = 'expect_identical']

R/object_usage_linter.R

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
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.
8+
#'
69
#' @evalRd rd_tags("object_usage_linter")
710
#' @seealso [linters] for a complete list of linters available in lintr.
811
#' @export
9-
object_usage_linter <- function() {
12+
object_usage_linter <- function(interpret_glue = TRUE) {
1013
Linter(function(source_file) {
1114
# If there is no xml data just return
1215
if (is.null(source_file$full_xml_parsed_content)) return(list())
@@ -47,7 +50,13 @@ object_usage_linter <- function() {
4750
if (inherits(fun, "try-error")) {
4851
return()
4952
}
50-
res <- parse_check_usage(fun)
53+
if (isTRUE(interpret_glue)) {
54+
known_used_symbols <- extract_glued_symbols(info$expr[[1L]])
55+
} else {
56+
known_used_symbols <- character()
57+
}
58+
59+
res <- parse_check_usage(fun, known_used_symbols = known_used_symbols)
5160

5261
lapply(
5362
which(!is.na(res$message)),
@@ -98,6 +107,63 @@ object_usage_linter <- function() {
98107
})
99108
}
100109

110+
extract_glued_symbols <- function(expr) {
111+
# TODO support more glue functions
112+
# Package glue:
113+
# - glue_sql()
114+
# - glue_safe()
115+
# - glue_col()
116+
# - glue_data()
117+
# - glue_data_sql()
118+
# - glue_data_safe()
119+
# - glue_data_col()
120+
#
121+
# Package stringr:
122+
# - str_interp()
123+
glue_calls <- xml2::xml_find_all(
124+
expr,
125+
xpath = paste0(
126+
"descendant::SYMBOL_FUNCTION_CALL[text() = 'glue']/", # a glue() call
127+
"preceding-sibling::NS_GET/preceding-sibling::SYMBOL_PACKAGE[text() = 'glue']/", # qualified with glue::
128+
"parent::expr[",
129+
# without .envir or .transform arguments
130+
"not(following-sibling::SYMBOL_SUB[text() = '.envir' or text() = '.transform']) and",
131+
# argument that is not a string constant
132+
"not(following-sibling::expr[not(STR_CONST)])",
133+
"]/",
134+
# get the complete call
135+
"parent::expr"
136+
)
137+
)
138+
139+
if (length(glue_calls) == 0L) return(character())
140+
glued_symbols <- new.env(parent = emptyenv())
141+
for (cl in glue_calls) {
142+
parsed_cl <- tryCatch(
143+
parse(text = xml2::xml_text(cl)),
144+
error = function(...) NULL,
145+
warning = function(...) NULL
146+
)[[1L]]
147+
if (is.null(parsed_cl)) next
148+
parsed_cl[[".transformer"]] <- function(text, envir) {
149+
parsed_text <- tryCatch(
150+
parse(text = text, keep.source = TRUE),
151+
error = function(...) NULL,
152+
warning = function(...) NULL
153+
)
154+
parsed_xml <- safe_parse_to_xml(parsed_text)
155+
if (is.null(parsed_xml)) return("")
156+
symbols <- xml2::xml_text(xml2::xml_find_all(parsed_xml, "//SYMBOL"))
157+
for (sym in symbols) {
158+
assign(sym, NULL, envir = glued_symbols)
159+
}
160+
""
161+
}
162+
eval(parsed_cl)
163+
}
164+
ls(envir = glued_symbols, all.names = TRUE)
165+
}
166+
101167
get_assignment_symbols <- function(xml) {
102168
left_assignment_symbols <-
103169
xml2::xml_text(xml2::xml_find_all(xml, "expr[LEFT_ASSIGN]/expr[1]/SYMBOL[1]"))
@@ -140,24 +206,30 @@ get_function_assignments <- function(xml) {
140206

141207
get_attr <- function(x, attr) as.integer(xml2::xml_attr(x, attr))
142208

143-
data.frame(
209+
res <- data.frame(
144210
line1 = viapply(funs, get_attr, "line1"),
145211
line2 = viapply(funs, get_attr, "line2"),
146212
col1 = viapply(funs, get_attr, "col1"),
147213
col2 = viapply(funs, get_attr, "col2"),
148214
stringsAsFactors = FALSE
149215
)
216+
res[["expr"]] <- funs
217+
res
150218
}
151219

152-
parse_check_usage <- function(expression) {
220+
parse_check_usage <- function(expression, known_used_symbols = character()) {
153221

154222
vals <- list()
155223

156224
report <- function(x) {
157225
vals[[length(vals) + 1L]] <<- x
158226
}
159227

160-
try(codetools::checkUsage(expression, report = report))
228+
try(codetools::checkUsage(
229+
expression,
230+
report = report,
231+
suppressLocalUnused = known_used_symbols
232+
))
161233

162234
function_name <- rex(anything, ": ")
163235
line_info <- rex(" ", "(", capture(name = "path", non_spaces), ":",

man/object_usage_linter.Rd

Lines changed: 5 additions & 1 deletion
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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,55 @@ test_that("robust against errors", {
300300
object_usage_linter()
301301
)
302302
})
303+
304+
test_that("interprets glue expressions", {
305+
linter <- object_usage_linter()
306+
307+
expect_lint(trim_some("
308+
fun <- function() {
309+
local_var <- 42
310+
glue::glue('The answer is {local_var}.')
311+
}
312+
"), NULL, linter)
313+
314+
# Check non-standard .open and .close
315+
expect_lint(trim_some("
316+
fun <- function() {
317+
local_var <- 42
318+
glue::glue('The answer is $[local_var].', .open = '$[', .close = ']')
319+
}
320+
"), NULL, linter)
321+
322+
# Steer clear of custom .transformer and .envir constructs
323+
expect_lint(trim_some("
324+
fun <- function() {
325+
local_var <- 42
326+
glue::glue('The answer is {local_var}.', .transformer = glue::identity_transformer)
327+
}
328+
"), "local_var", linter)
329+
330+
expect_lint(trim_some("
331+
fun <- function() {
332+
local_var <- 42
333+
e <- new.env()
334+
glue::glue('The answer is {local_var}.', .envir = e)
335+
}
336+
"), "local_var", linter)
337+
338+
# unused is caught, glue-used is not
339+
expect_lint(trim_some("
340+
fun <- function() {
341+
local_var <- 42
342+
unused_var <- 3
343+
glue::glue('The answer is {local_var}.')
344+
}
345+
"), "unused_var", linter)
346+
347+
# glue-only is caught with option off
348+
expect_lint(trim_some("
349+
fun <- function() {
350+
local_var <- 42
351+
glue::glue('The answer is {local_var}.')
352+
}
353+
"), "local_var", object_usage_linter(interpret_glue = FALSE))
354+
})

0 commit comments

Comments
 (0)