|
3 | 3 | #' Check that closures have the proper usage using [codetools::checkUsage()].
|
4 | 4 | #' Note that this runs [base::eval()] on the code, so do not use with untrusted code.
|
5 | 5 | #'
|
| 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 | +#' |
6 | 9 | #' @evalRd rd_tags("object_usage_linter")
|
7 | 10 | #' @seealso [linters] for a complete list of linters available in lintr.
|
8 | 11 | #' @export
|
9 |
| -object_usage_linter <- function() { |
| 12 | +object_usage_linter <- function(interpret_glue = TRUE) { |
10 | 13 | Linter(function(source_file) {
|
11 | 14 | # If there is no xml data just return
|
12 | 15 | if (is.null(source_file$full_xml_parsed_content)) return(list())
|
@@ -47,7 +50,13 @@ object_usage_linter <- function() {
|
47 | 50 | if (inherits(fun, "try-error")) {
|
48 | 51 | return()
|
49 | 52 | }
|
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) |
51 | 60 |
|
52 | 61 | lapply(
|
53 | 62 | which(!is.na(res$message)),
|
@@ -98,6 +107,63 @@ object_usage_linter <- function() {
|
98 | 107 | })
|
99 | 108 | }
|
100 | 109 |
|
| 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 | + |
101 | 167 | get_assignment_symbols <- function(xml) {
|
102 | 168 | left_assignment_symbols <-
|
103 | 169 | xml2::xml_text(xml2::xml_find_all(xml, "expr[LEFT_ASSIGN]/expr[1]/SYMBOL[1]"))
|
@@ -140,24 +206,30 @@ get_function_assignments <- function(xml) {
|
140 | 206 |
|
141 | 207 | get_attr <- function(x, attr) as.integer(xml2::xml_attr(x, attr))
|
142 | 208 |
|
143 |
| - data.frame( |
| 209 | + res <- data.frame( |
144 | 210 | line1 = viapply(funs, get_attr, "line1"),
|
145 | 211 | line2 = viapply(funs, get_attr, "line2"),
|
146 | 212 | col1 = viapply(funs, get_attr, "col1"),
|
147 | 213 | col2 = viapply(funs, get_attr, "col2"),
|
148 | 214 | stringsAsFactors = FALSE
|
149 | 215 | )
|
| 216 | + res[["expr"]] <- funs |
| 217 | + res |
150 | 218 | }
|
151 | 219 |
|
152 |
| -parse_check_usage <- function(expression) { |
| 220 | +parse_check_usage <- function(expression, known_used_symbols = character()) { |
153 | 221 |
|
154 | 222 | vals <- list()
|
155 | 223 |
|
156 | 224 | report <- function(x) {
|
157 | 225 | vals[[length(vals) + 1L]] <<- x
|
158 | 226 | }
|
159 | 227 |
|
160 |
| - try(codetools::checkUsage(expression, report = report)) |
| 228 | + try(codetools::checkUsage( |
| 229 | + expression, |
| 230 | + report = report, |
| 231 | + suppressLocalUnused = known_used_symbols |
| 232 | + )) |
161 | 233 |
|
162 | 234 | function_name <- rex(anything, ": ")
|
163 | 235 | line_info <- rex(" ", "(", capture(name = "path", non_spaces), ":",
|
|
0 commit comments