Skip to content

Commit 538440d

Browse files
add xml_find_function_calls() helper to source expressions (#2357)
* add first implementation of xml_find_function_calls * delint * support getting all function calls and using names. * squash conversions * review comments * clean up * add vignette section and NEWS bullet * smarter conjunct_test_linter migration * smarter consecutive_assertion_linter migration * remove self::SYMBOL_FUNCTION_CALL[text() = ...] xpaths * delint * Update expect_s3_class_linter.R * Reference GH issue # in TODO * review comments * add missing comma in example * Update NEWS.md * supersede #2365 * Update R/xp_utils.R Co-authored-by: Michael Chirico <chiricom@google.com> * fix bad commit * Update NEWS.md * Add an upper bound improvement from r-devel --------- Co-authored-by: Michael Chirico <michaelchirico4@gmail.com> Co-authored-by: Michael Chirico <chiricom@google.com>
1 parent d1491c2 commit 538440d

File tree

70 files changed

+534
-414
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+534
-414
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ Collate:
175175
'settings_utils.R'
176176
'shared_constants.R'
177177
'sort_linter.R'
178+
'source_utils.R'
178179
'spaces_inside_linter.R'
179180
'spaces_left_parentheses_linter.R'
180181
'sprintf_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export(linters_with_defaults)
9595
export(linters_with_tags)
9696
export(list_comparison_linter)
9797
export(literal_coercion_linter)
98+
export(make_linter_from_function_xpath)
9899
export(make_linter_from_xpath)
99100
export(matrix_apply_linter)
100101
export(missing_argument_linter)

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
* `string_boundary_linter()` recognizes regular expression calls like `grepl("^abc$", x)` that can be replaced by using `==` instead (#1613, @MichaelChirico).
4141
* `unreachable_code_linter()` has an argument `allow_comment_regex` for customizing which "terminal" comments to exclude (#2327, @MichaelChirico). `# nolint end` comments are always excluded, as are {covr} exclusions (e.g. `# nocov end`) by default.
4242
* `format()` and `print()` methods for `lint` and `lints` classes get a new option `width` to control the printing width of lint messages (#1884, @MichaelChirico). The default is controlled by a new option `lintr.format_width`; if unset, no wrapping occurs (matching earlier behavior).
43+
* New function node caching for big efficiency gains to most linters (e.g. overall `lint_package()` improvement of 14-27% and core linting improvement up to 30%; #2357, @AshesITR). Most linters are written around function usage, and XPath performance searching for many functions is poor. The new `xml_find_function_calls()` entry in the `get_source_expressions()` output caches all function call nodes instead. See the vignette on creating linters for more details on how to use it.
44+
* The full linter suite is roughly 14% faster due to caching of the frequently used `//SYMBOL_FUNCTION_CALL` XPath to examine function calls. (@AshesITR, #2357)
45+
+ The `source_expression` passed to linters gains a fast way to query function call nodes using `source_expression$xml_find_function_calls()`. Use this to speed up linters using XPaths that start with `//SYMBOL_FUNCTION_CALL`.
46+
+ The vignette on creating linters contains additional information on how to use it.
47+
+ Instead of `xml_find_all(source_expression$xml_parsed_content, "//SYMBOL_FUNCTION_CALL[text() = 'foo' or text() = 'bar']`, use `source_expression$xml_find_function_calls(c("foo", "bar"))`.
48+
+ Instead of `make_linter_from_xpath(xpath = "//SYMBOL_FUNCTION_CALL[text() = 'foo' or text() = 'bar']/cond")`, use the new `make_linter_from_function_xpath(function_names = c("foo", "bar"), xpath = "cond")`.
4349

4450
### New linters
4551

R/any_duplicated_linter.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@
3535
#' @export
3636
any_duplicated_linter <- function() {
3737
any_duplicated_xpath <- "
38-
//SYMBOL_FUNCTION_CALL[text() = 'any']
39-
/parent::expr
38+
parent::expr
4039
/following-sibling::expr[1][expr[1][SYMBOL_FUNCTION_CALL[text() = 'duplicated']]]
4140
/parent::expr[
4241
count(expr) = 2
@@ -87,8 +86,9 @@ any_duplicated_linter <- function() {
8786

8887
Linter(linter_level = "expression", function(source_expression) {
8988
xml <- source_expression$xml_parsed_content
89+
xml_calls <- source_expression$xml_find_function_calls("any")
9090

91-
any_duplicated_expr <- xml_find_all(xml, any_duplicated_xpath)
91+
any_duplicated_expr <- xml_find_all(xml_calls, any_duplicated_xpath)
9292
any_duplicated_lints <- xml_nodes_to_lints(
9393
any_duplicated_expr,
9494
source_expression = source_expression,

R/any_is_na_linter.R

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@
3737
#' @export
3838
any_is_na_linter <- function() {
3939
xpath <- "
40-
//SYMBOL_FUNCTION_CALL[text() = 'any']
41-
/parent::expr
40+
parent::expr
4241
/following-sibling::expr[1][expr[1][SYMBOL_FUNCTION_CALL[text() = 'is.na']]]
4342
/parent::expr[
4443
count(expr) = 2
@@ -47,9 +46,8 @@ any_is_na_linter <- function() {
4746
"
4847

4948
Linter(linter_level = "expression", function(source_expression) {
50-
xml <- source_expression$xml_parsed_content
51-
52-
bad_expr <- xml_find_all(xml, xpath)
49+
xml_calls <- source_expression$xml_find_function_calls("any")
50+
bad_expr <- xml_find_all(xml_calls, xpath)
5351

5452
xml_nodes_to_lints(
5553
bad_expr,

R/backport_linter.R

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,28 @@ backport_linter <- function(r_version = getRversion(), except = character()) {
4545
backport_index <- rep(names(backport_blacklist), times = lengths(backport_blacklist))
4646
names(backport_index) <- unlist(backport_blacklist)
4747

48-
names_xpath <- "//SYMBOL | //SYMBOL_FUNCTION_CALL"
49-
5048
Linter(linter_level = "expression", function(source_expression) {
5149
xml <- source_expression$xml_parsed_content
5250

53-
all_names_nodes <- xml_find_all(xml, names_xpath)
51+
used_symbols <- xml_find_all(xml, "//SYMBOL")
52+
used_symbols <- used_symbols[xml_text(used_symbols) %in% names(backport_index)]
53+
54+
all_names_nodes <- combine_nodesets(
55+
source_expression$xml_find_function_calls(names(backport_index)),
56+
used_symbols
57+
)
5458
all_names <- xml_text(all_names_nodes)
5559

5660
bad_versions <- unname(backport_index[all_names])
57-
needs_backport <- !is.na(bad_versions)
5861

5962
lint_message <- sprintf(
6063
"%s (R %s) is not available for dependency R >= %s.",
61-
all_names[needs_backport],
62-
bad_versions[needs_backport],
64+
all_names,
65+
bad_versions,
6366
r_version
6467
)
6568
xml_nodes_to_lints(
66-
all_names_nodes[needs_backport],
69+
all_names_nodes,
6770
source_expression = source_expression,
6871
lint_message = lint_message,
6972
type = "warning"

R/boolean_arithmetic_linter.R

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,30 @@ boolean_arithmetic_linter <- function() {
3535
zero_expr <- "(EQ or NE or GT or LE) and expr[NUM_CONST[text() = '0' or text() = '0L']]"
3636
one_expr <- "(LT or GE) and expr[NUM_CONST[text() = '1' or text() = '1L']]"
3737
length_xpath <- glue("
38-
//SYMBOL_FUNCTION_CALL[text() = 'which' or text() = 'grep']
39-
/parent::expr
38+
parent::expr
4039
/parent::expr
4140
/parent::expr[
4241
expr[SYMBOL_FUNCTION_CALL[text() = 'length']]
4342
and parent::expr[ ({zero_expr}) or ({one_expr})]
4443
]
4544
")
4645
sum_xpath <- glue("
47-
//SYMBOL_FUNCTION_CALL[text() = 'sum']
48-
/parent::expr
46+
parent::expr
4947
/parent::expr[
5048
expr[
5149
expr[SYMBOL_FUNCTION_CALL[text() = 'grepl']]
5250
or (EQ or NE or GT or LT or GE or LE)
5351
] and parent::expr[ ({zero_expr}) or ({one_expr})]
5452
]
5553
")
56-
any_xpath <- paste(length_xpath, "|", sum_xpath)
5754

5855
Linter(linter_level = "expression", function(source_expression) {
59-
xml <- source_expression$xml_parsed_content
60-
61-
any_expr <- xml_find_all(xml, any_xpath)
56+
length_calls <- source_expression$xml_find_function_calls(c("which", "grep"))
57+
sum_calls <- source_expression$xml_find_function_calls("sum")
58+
any_expr <- c(
59+
xml_find_all(length_calls, length_xpath),
60+
xml_find_all(sum_calls, sum_xpath)
61+
)
6262

6363
xml_nodes_to_lints(
6464
any_expr,

R/class_equals_linter.R

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@
3535
#' @export
3636
class_equals_linter <- function() {
3737
xpath <- "
38-
//SYMBOL_FUNCTION_CALL[text() = 'class']
39-
/parent::expr
38+
parent::expr
4039
/parent::expr
4140
/parent::expr[
4241
not(preceding-sibling::OP-LEFT-BRACKET)
@@ -45,9 +44,8 @@ class_equals_linter <- function() {
4544
"
4645

4746
Linter(linter_level = "expression", function(source_expression) {
48-
xml <- source_expression$xml_parsed_content
49-
50-
bad_expr <- xml_find_all(xml, xpath)
47+
xml_calls <- source_expression$xml_find_function_calls("class")
48+
bad_expr <- xml_find_all(xml_calls, xpath)
5149

5250
operator <- xml_find_chr(bad_expr, "string(*[2])")
5351
lint_message <- sprintf(

R/condition_call_linter.R

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,11 @@ condition_call_linter <- function(display_call = FALSE) {
7777
msg_fmt <- "Use %s(., call. = FALSE) not to display the call in an error message."
7878
}
7979

80-
xpath <- glue::glue("
81-
//SYMBOL_FUNCTION_CALL[text() = 'stop' or text() = 'warning']
82-
/parent::expr[{call_cond}]
83-
/parent::expr
84-
")
80+
xpath <- glue::glue("parent::expr[{call_cond}]/parent::expr")
8581

8682
Linter(linter_level = "expression", function(source_expression) {
87-
xml <- source_expression$xml_parsed_content
88-
89-
bad_expr <- xml_find_all(xml, xpath)
83+
xml_calls <- source_expression$xml_find_function_calls(c("stop", "warning"))
84+
bad_expr <- xml_find_all(xml_calls, xpath)
9085

9186
xml_nodes_to_lints(
9287
bad_expr,

R/condition_message_linter.R

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@
4444
condition_message_linter <- function() {
4545
translators <- c("packageStartupMessage", "message", "warning", "stop")
4646
xpath <- glue("
47-
//SYMBOL_FUNCTION_CALL[
48-
({xp_text_in_table(translators)})
49-
and not(preceding-sibling::OP-DOLLAR or preceding-sibling::OP-AT)
47+
self::SYMBOL_FUNCTION_CALL[
48+
not(preceding-sibling::OP-DOLLAR or preceding-sibling::OP-AT)
5049
]
5150
/parent::expr
5251
/following-sibling::expr[
@@ -57,9 +56,8 @@ condition_message_linter <- function() {
5756
")
5857

5958
Linter(linter_level = "expression", function(source_expression) {
60-
xml <- source_expression$xml_parsed_content
61-
62-
bad_expr <- xml_find_all(xml, xpath)
59+
xml_calls <- source_expression$xml_find_function_calls(translators)
60+
bad_expr <- xml_find_all(xml_calls, xpath)
6361
sep_value <- get_r_string(bad_expr, xpath = "./expr/SYMBOL_SUB[text() = 'sep']/following-sibling::expr/STR_CONST")
6462

6563
bad_expr <- bad_expr[is.na(sep_value) | sep_value %in% c("", " ")]

R/conjunct_test_linter.R

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -79,48 +79,43 @@ conjunct_test_linter <- function(allow_named_stopifnot = TRUE,
7979
allow_filter <- match.arg(allow_filter)
8080

8181
expect_true_assert_that_xpath <- "
82-
//SYMBOL_FUNCTION_CALL[text() = 'expect_true' or text() = 'assert_that']
83-
/parent::expr
82+
parent::expr
8483
/following-sibling::expr[1][AND2]
8584
/parent::expr
8685
"
8786
named_stopifnot_condition <- if (allow_named_stopifnot) "and not(preceding-sibling::*[1][self::EQ_SUB])" else ""
8887
stopifnot_xpath <- glue("
89-
//SYMBOL_FUNCTION_CALL[text() = 'stopifnot']
90-
/parent::expr
88+
parent::expr
9189
/following-sibling::expr[1][AND2 {named_stopifnot_condition}]
9290
/parent::expr
9391
")
9492
expect_false_xpath <- "
95-
//SYMBOL_FUNCTION_CALL[text() = 'expect_false']
96-
/parent::expr
93+
parent::expr
9794
/following-sibling::expr[1][OR2]
9895
/parent::expr
9996
"
100-
test_xpath <- paste(
101-
expect_true_assert_that_xpath,
102-
stopifnot_xpath,
103-
expect_false_xpath,
104-
sep = " | "
105-
)
10697

10798
filter_ns_cond <- switch(allow_filter,
10899
never = "not(SYMBOL_PACKAGE[text() != 'dplyr'])",
109100
not_dplyr = "SYMBOL_PACKAGE[text() = 'dplyr']",
110101
always = "true"
111102
)
112103
filter_xpath <- glue("
113-
//SYMBOL_FUNCTION_CALL[text() = 'filter']
114-
/parent::expr[{ filter_ns_cond }]
104+
parent::expr[{ filter_ns_cond }]
115105
/parent::expr
116106
/expr[AND]
117107
")
118108

119109
Linter(linter_level = "file", function(source_expression) {
120110
# need the full file to also catch usages at the top level
121-
xml <- source_expression$full_xml_parsed_content
122-
123-
test_expr <- xml_find_all(xml, test_xpath)
111+
expect_true_assert_that_calls <- source_expression$xml_find_function_calls(c("expect_true", "assert_that"))
112+
stopifnot_calls <- source_expression$xml_find_function_calls("stopifnot")
113+
expect_false_calls <- source_expression$xml_find_function_calls("expect_false")
114+
test_expr <- combine_nodesets(
115+
xml_find_all(expect_true_assert_that_calls, expect_true_assert_that_xpath),
116+
xml_find_all(stopifnot_calls, stopifnot_xpath),
117+
xml_find_all(expect_false_calls, expect_false_xpath)
118+
)
124119

125120
matched_fun <- xp_call_name(test_expr)
126121
operator <- xml_find_chr(test_expr, "string(expr/*[self::AND2 or self::OR2])")
@@ -143,7 +138,8 @@ conjunct_test_linter <- function(allow_named_stopifnot = TRUE,
143138
)
144139

145140
if (allow_filter != "always") {
146-
filter_expr <- xml_find_all(xml, filter_xpath)
141+
xml_calls <- source_expression$xml_find_function_calls("filter")
142+
filter_expr <- xml_find_all(xml_calls, filter_xpath)
147143

148144
filter_lints <- xml_nodes_to_lints(
149145
filter_expr,

R/consecutive_assertion_linter.R

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,14 @@
3131
#' @seealso [linters] for a complete list of linters available in lintr.
3232
#' @export
3333
consecutive_assertion_linter <- function() {
34-
xpath <- "
35-
//SYMBOL_FUNCTION_CALL[text() = 'stopifnot']
36-
/parent::expr
34+
stopifnot_xpath <- "
35+
parent::expr
3736
/parent::expr[
3837
expr[1]/SYMBOL_FUNCTION_CALL = following-sibling::expr[1]/expr[1]/SYMBOL_FUNCTION_CALL
3938
]
40-
|
41-
//SYMBOL_FUNCTION_CALL[text() = 'assert_that']
42-
/parent::expr
39+
"
40+
assert_that_xpath <- "
41+
parent::expr
4342
/parent::expr[
4443
not(SYMBOL_SUB[text() = 'msg'])
4544
and not(following-sibling::expr[1]/SYMBOL_SUB[text() = 'msg'])
@@ -49,9 +48,12 @@ consecutive_assertion_linter <- function() {
4948

5049
Linter(linter_level = "file", function(source_expression) {
5150
# need the full file to also catch usages at the top level
52-
xml <- source_expression$full_xml_parsed_content
53-
54-
bad_expr <- xml_find_all(xml, xpath)
51+
stopifnot_calls <- source_expression$xml_find_function_calls("stopifnot")
52+
assert_that_calls <- source_expression$xml_find_function_calls("assert_that")
53+
bad_expr <- combine_nodesets(
54+
xml_find_all(stopifnot_calls, stopifnot_xpath),
55+
xml_find_all(assert_that_calls, assert_that_xpath)
56+
)
5557

5658
matched_function <- xp_call_name(bad_expr)
5759
xml_nodes_to_lints(

R/consecutive_mutate_linter.R

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,11 @@
3737
#' @seealso [linters] for a complete list of linters available in lintr.
3838
#' @export
3939
consecutive_mutate_linter <- function(invalid_backends = "dbplyr") {
40-
attach_pkg_xpath <- glue("
41-
//SYMBOL_FUNCTION_CALL[text() = 'library' or text() = 'require']
42-
/parent::expr
40+
attach_pkg_xpath <- "
41+
parent::expr
4342
/following-sibling::expr
4443
/*[self::SYMBOL or self::STR_CONST]
45-
")
44+
"
4645

4746
namespace_xpath <- glue("
4847
//SYMBOL_PACKAGE[{ xp_text_in_table(invalid_backends) }]
@@ -75,7 +74,10 @@ consecutive_mutate_linter <- function(invalid_backends = "dbplyr") {
7574
# need the full file to also catch usages at the top level
7675
xml <- source_expression$full_xml_parsed_content
7776

78-
attach_str <- get_r_string(xml_find_all(xml, attach_pkg_xpath))
77+
attach_str <- get_r_string(xml_find_all(
78+
source_expression$xml_find_function_calls(c("library", "require")),
79+
attach_pkg_xpath
80+
))
7981
if (any(invalid_backends %in% attach_str)) {
8082
return(list())
8183
}

R/expect_comparison_linter.R

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ expect_comparison_linter <- function() {
5151
# != doesn't have a clean replacement
5252
comparator_nodes <- setdiff(infix_metadata$xml_tag[infix_metadata$comparator], "NE")
5353
xpath <- glue("
54-
//SYMBOL_FUNCTION_CALL[text() = 'expect_true']
55-
/parent::expr
54+
parent::expr
5655
/following-sibling::expr[1][ {xp_or(comparator_nodes)} ]
5756
/parent::expr[not(SYMBOL_SUB[text() = 'info'])]
5857
")
@@ -64,9 +63,8 @@ expect_comparison_linter <- function() {
6463
)
6564

6665
Linter(linter_level = "expression", function(source_expression) {
67-
xml <- source_expression$xml_parsed_content
68-
69-
bad_expr <- xml_find_all(xml, xpath)
66+
xml_calls <- source_expression$xml_find_function_calls("expect_true")
67+
bad_expr <- xml_find_all(xml_calls, xpath)
7068

7169
comparator <- xml_find_chr(bad_expr, "string(expr[2]/*[2])")
7270
expectation <- comparator_expectation_map[comparator]

0 commit comments

Comments
 (0)