Skip to content

Commit fb294cc

Browse files
ignore irrelevant symbols when pinpointing undefined variable lints (#1915)
1 parent c9b3408 commit fb294cc

File tree

3 files changed

+125
-9
lines changed

3 files changed

+125
-9
lines changed

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737

3838
* `object_name_linter()` allows all S3 group Generics (see `?base::groupGeneric`) and S3 generics defined in a different file in the same package (#1808, #1841, @AshesITR)
3939

40+
* `object_usage_linter()` improves identification of the exact source of a lint for undefined variables in expressions with where the variable is used as a symbol in a usual way, for example in a formula or in an extraction with `$` (#1914, @MichaelChirico).
41+
4042
## Changes to defaults
4143

4244
* Set the default for the `except` argument in `duplicate_argument_linter()` to `c("mutate", "transmute")`.

R/object_usage_linter.R

+10-9
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ object_usage_linter <- function(interpret_glue = TRUE, skip_with = TRUE) {
4646
sep = " | "
4747
)
4848

49+
# not all instances of linted symbols are potential sources for the observed violations -- see #1914
50+
symbol_exclude_cond <- "preceding-sibling::OP-DOLLAR or preceding-sibling::OP-AT or ancestor::expr[OP-TILDE]"
51+
xpath_culprit_symbol <- glue::glue("
52+
descendant::SYMBOL[not( {symbol_exclude_cond} )]
53+
| descendant::SYMBOL_FUNCTION_CALL[not( {symbol_exclude_cond} )]
54+
| descendant::SPECIAL
55+
| descendant::LEFT_ASSIGN[text() = ':=']
56+
")
57+
4958
Linter(function(source_expression) {
5059
if (!is_lint_level(source_expression, "file")) {
5160
return(list())
@@ -97,15 +106,7 @@ object_usage_linter <- function(interpret_glue = TRUE, skip_with = TRUE) {
97106
# e.g. `not_existing<-`(a, b)
98107
res$name <- rex::re_substitutes(res$name, rex::rex("<-"), "")
99108

100-
lintable_symbols <- xml2::xml_find_all(
101-
fun_assignment,
102-
"
103-
descendant::SYMBOL
104-
| descendant::SYMBOL_FUNCTION_CALL
105-
| descendant::SPECIAL
106-
| descendant::LEFT_ASSIGN[text() = ':=']
107-
"
108-
)
109+
lintable_symbols <- xml2::xml_find_all(fun_assignment, xpath_culprit_symbol)
109110

110111
lintable_symbol_names <- gsub("^`|`$", "", xml2::xml_text(lintable_symbols))
111112
lintable_symbol_lines <- as.integer(xml2::xml_attr(lintable_symbols, "line1"))

tests/testthat/test-object_usage_linter.R

+113
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,119 @@ test_that("messages without a quoted name are caught", {
639639
)
640640
})
641641

642+
# See #1914
643+
test_that("symbols in formulas aren't treated as 'undefined global'", {
644+
expect_lint(
645+
trim_some("
646+
foo <- function(x) {
647+
lm(
648+
y ~ z,
649+
data = x[!is.na(y)]
650+
)
651+
}
652+
"),
653+
list(
654+
message = "no visible binding for global variable 'y'",
655+
line_number = 4L,
656+
column_number = 21L
657+
),
658+
object_usage_linter()
659+
)
660+
661+
# neither on the RHS
662+
expect_lint(
663+
trim_some("
664+
foo <- function(x) {
665+
lm(
666+
z ~ y,
667+
data = x[!is.na(y)]
668+
)
669+
}
670+
"),
671+
list(
672+
message = "no visible binding for global variable 'y'",
673+
line_number = 4L,
674+
column_number = 21L
675+
),
676+
object_usage_linter()
677+
)
678+
679+
# nor in nested expressions
680+
expect_lint(
681+
trim_some("
682+
foo <- function(x) {
683+
lm(
684+
log(log(y)) ~ z,
685+
data = x[!is.na(y)]
686+
)
687+
}
688+
"),
689+
list(
690+
message = "no visible binding for global variable 'y'",
691+
line_number = 4L,
692+
column_number = 21L
693+
),
694+
object_usage_linter()
695+
)
696+
697+
# nor as a call
698+
# NB: I wanted this to be s(), as in mgcv::s(), but that
699+
# doesn't work in this test suite because it resolves to
700+
# rex::s() since we attach that in testthat.R
701+
expect_lint(
702+
trim_some("
703+
foo <- function(x) {
704+
lm(
705+
y(w) ~ z,
706+
data = x[!is.na(y)]
707+
)
708+
}
709+
"),
710+
list(
711+
message = "no visible binding for global variable 'y'",
712+
line_number = 4L,
713+
column_number = 21L
714+
),
715+
object_usage_linter()
716+
)
717+
})
718+
719+
test_that("NSE-ish symbols after $/@ are ignored as sources for lints", {
720+
expect_lint(
721+
trim_some("
722+
foo <- function(x) {
723+
ggplot2::ggplot(
724+
x[!is.na(x$column), ],
725+
ggplot2::aes(x = column, fill = factor(x$grp))
726+
)
727+
}
728+
"),
729+
list(
730+
message = "no visible binding for global variable 'column'",
731+
line_number = 4L,
732+
column_number = 22L
733+
),
734+
object_usage_linter()
735+
)
736+
737+
expect_lint(
738+
trim_some("
739+
foo <- function(x) {
740+
ggplot2::ggplot(
741+
x[!is.na(x@column), ],
742+
ggplot2::aes(x = column, fill = factor(x$grp))
743+
)
744+
}
745+
"),
746+
list(
747+
message = "no visible binding for global variable 'column'",
748+
line_number = 4L,
749+
column_number = 22L
750+
),
751+
object_usage_linter()
752+
)
753+
})
754+
642755
test_that("functional lambda definitions are also caught", {
643756
skip_if_not_r_version("4.1.0")
644757

0 commit comments

Comments
 (0)