Skip to content

Commit b1d4d0d

Browse files
Correctly parse checkUsage when a multiline warning is encountered (#758)
* Correctly parse checkUsage when a multiline warning is encountered fixes #507 * 100% coverage for object_usage_linter * improve location detection, nolint new false positive lints. * fix single-line case add tests for 100% coverage * nzchar over !="" * add comments * clean up globalVariables() and remove nolint sections Co-authored-by: Michael Chirico <[email protected]>
1 parent 25b2e1d commit b1d4d0d

File tree

5 files changed

+163
-45
lines changed

5 files changed

+163
-45
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
* `trailing_blank_lines_linter()` now also lints files without a terminal newline (#675, @AshesITR)
5656
* `object_name_linter()` now correctly detects imported functions when linting packages (#642, @AshesITR)
5757
* Consistent access to linters through a function call, even for linters without parameters (#245, @fangly, @AshesITR, and @MichaelChirico)
58+
* `object_usage_linter()` now correctly reports usage warnings spanning multiple lines (#507, @AshesITR)
5859
* `T_and_F_symbol_linter()` no longer lints occurrences of `T` and `F` when used for subsetting and gives a better
5960
message when used as variable names (#657, @AshesITR)
6061

R/aaa.R

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ NULL
2424
# need to register rex shortcuts as globals to avoid CRAN check errors
2525
rex::register_shortcuts("lintr")
2626

27-
utils::globalVariables("from", "lintr")
27+
utils::globalVariables(
28+
c("line1", "col1", "line2", "col2", # columns of parsed_content
29+
"id", "parent", "token", "terminal", "text"), # dito
30+
"lintr"
31+
)

R/object_usage_linter.R

Lines changed: 74 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,34 +45,51 @@ object_usage_linter <- function() {
4545
}
4646
res <- parse_check_usage(fun)
4747

48-
lapply(which(!is.na(res$message)),
49-
function(row_num) {
50-
row <- res[row_num, ]
51-
52-
if (row$name %in% declared_globals) {
53-
return()
54-
}
55-
56-
org_line_num <- as.integer(row$line_number) + info$line1 - 1L
57-
58-
line <- source_file$content[as.integer(org_line_num)]
59-
60-
row$name <- re_substitutes(row$name, rex("<-"), "")
61-
62-
location <- re_matches(line,
63-
rex(row$name),
64-
locations = TRUE)
65-
66-
Lint(
67-
filename = source_file$filename,
68-
line_number = org_line_num,
69-
column_number = location$start,
70-
type = "warning",
71-
message = row$message,
72-
line = line,
73-
ranges = list(c(location$start, location$end))
74-
)
75-
})
48+
lapply(
49+
which(!is.na(res$message)),
50+
function(row_num) {
51+
row <- res[row_num, ]
52+
53+
if (row$name %in% declared_globals) {
54+
return()
55+
}
56+
57+
org_line_num <- as.integer(row$line1) + info$line1 - 1L
58+
line <- source_file$content[as.integer(org_line_num)]
59+
60+
row$name <- re_substitutes(row$name, rex("<-"), "")
61+
62+
location <- re_matches(line, rex(boundary, row$name, boundary), locations = TRUE)
63+
64+
# Handle multi-line lints where name occurs on subsequent lines (#507)
65+
if (is.na(location$start) && nzchar(row$line2) && row$line2 != row$line1) {
66+
lines <- source_file$content[org_line_num:(as.integer(row$line2) + info$line1 - 1L)]
67+
locations <- re_matches(lines, rex(boundary, row$name, boundary), locations = TRUE)
68+
69+
matching_row <- (which(!is.na(locations$start)) %||% 1L)[[1L]] # first matching row or 1 (as a fallback)
70+
71+
org_line_num <- org_line_num + matching_row - 1L
72+
location <- locations[matching_row, ]
73+
line <- lines[matching_row]
74+
}
75+
76+
# Fallback if name isn't found anywhere: lint the first line
77+
if (is.na(location$start)) {
78+
location$start <- 1L
79+
location$end <- nchar(line)
80+
}
81+
82+
Lint(
83+
filename = source_file$filename,
84+
line_number = org_line_num,
85+
column_number = location$start,
86+
type = "warning",
87+
message = row$message,
88+
line = line,
89+
ranges = list(c(location$start, location$end))
90+
)
91+
}
92+
)
7693
})
7794
})
7895
}
@@ -139,23 +156,38 @@ parse_check_usage <- function(expression) {
139156
try(codetools::checkUsage(expression, report = report))
140157

141158
function_name <- rex(anything, ": ")
142-
line_info <- rex(" ", "(", capture(name = "path", non_spaces), ":", capture(name = "line_number", digits), ")")
143-
144-
res <- re_matches(vals,
145-
rex(function_name,
146-
capture(name = "message", anything,
147-
one_of(quote, "\u2018"), capture(name = "name", anything), one_of(quote, "\u2019"),
148-
anything),
149-
line_info))
159+
line_info <- rex(" ", "(", capture(name = "path", non_spaces), ":",
160+
capture(name = "line1", digits), maybe("-", capture(name = "line2", digits)), ")")
161+
162+
res <- re_matches(
163+
vals,
164+
rex(
165+
function_name,
166+
capture(
167+
name = "message",
168+
anything,
169+
one_of(quote, "\u2018"),
170+
capture(name = "name", anything),
171+
one_of(quote, "\u2019"),
172+
anything
173+
),
174+
line_info
175+
)
176+
)
150177

151178
missing <- is.na(res$message)
152179
if (any(missing)) {
153-
res[missing, ] <- re_matches(vals[missing],
154-
rex(function_name,
155-
capture(name = "message",
156-
"possible error in ", capture(name = "name", anything), ": ", anything
157-
),
158-
line_info))
180+
res[missing, ] <- re_matches(
181+
vals[missing],
182+
rex(
183+
function_name,
184+
capture(
185+
name = "message",
186+
"possible error in ", capture(name = "name", anything), ": ", anything
187+
),
188+
line_info
189+
)
190+
)
159191
}
160192

161193
res

R/pipe_continuation_linter.R

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,3 @@ pipe_continuation_linter <- function() {
7272
})
7373
})
7474
}
75-
76-
utils::globalVariables(c("line1", "line2", "col1", "col2"), "lintr")

tests/testthat/test-object_usage_linter.R

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,86 @@ test_that("used symbols are detected correctly", {
215215
object_usage_linter()
216216
)
217217
})
218+
219+
test_that("object_usage_linter finds lints spanning multiple lines", {
220+
# Regression test for #507
221+
expect_lint(
222+
trim_some("
223+
foo <- function() {
224+
if (unknown_function()) NULL
225+
226+
if (unknown_function()) {
227+
NULL
228+
}
229+
}
230+
"),
231+
list(
232+
list(message = "unknown_function", line_number = 2L),
233+
list(message = "unknown_function", line_number = 4L)
234+
),
235+
object_usage_linter()
236+
)
237+
238+
# Linted symbol is not on the first line of the usage warning
239+
expect_lint(
240+
trim_some("
241+
foo <- function(x) {
242+
with(
243+
x,
244+
unknown_symbol
245+
)
246+
}
247+
"),
248+
list(message = "unknown_symbol", line_number = 4L, column_number = 5L),
249+
object_usage_linter()
250+
)
251+
252+
# Kill regex match to enforce fallback to line 1 column 1 of the warning
253+
expect_lint(
254+
trim_some("
255+
foo <- function(x) {
256+
with(
257+
x,
258+
`\u2019regex_kill`
259+
)
260+
}
261+
"),
262+
list(line_number = 2L, column_number = 1L),
263+
object_usage_linter()
264+
)
265+
})
266+
267+
test_that("global variable detection works", {
268+
old_globals <- utils::globalVariables(package = globalenv())
269+
utils::globalVariables("global_function", package = globalenv())
270+
on.exit(utils::globalVariables(old_globals, package = globalenv(), add = FALSE))
271+
272+
expect_lint(
273+
trim_some("
274+
foo <- function() {
275+
if (global_function()) NULL
276+
277+
if (global_function()) {
278+
NULL
279+
}
280+
}
281+
"),
282+
NULL,
283+
object_usage_linter()
284+
)
285+
})
286+
287+
test_that("package detection works", {
288+
expect_length(
289+
lint_package("dummy_packages/package", linters = object_usage_linter(), parse_settings = FALSE),
290+
9L
291+
)
292+
})
293+
294+
test_that("robust against errors", {
295+
expect_lint(
296+
"assign(\"x\", unknown_function)",
297+
NULL,
298+
object_usage_linter()
299+
)
300+
})

0 commit comments

Comments
 (0)