Skip to content

Commit 367db02

Browse files
Logic to make linters robust to adversarial comments (#2901)
* logic to make linters robust to adversarial comments * remove those under #2827 * more in sync with #2827 in/out rule * need known_safe * delint * add a comment * spurious ws * review feedback * update / improve comment about needs_braces=TRUE * bad merge: needs to be following-sibling
1 parent e67713d commit 367db02

31 files changed

+449
-192
lines changed

R/brace_linter.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ brace_linter <- function(allow_single_line = FALSE,
134134
{ xp_cond_closed }
135135
and (
136136
(@line1 = preceding-sibling::*[1][not(self::OP-LEFT-BRACE)]/@line2)
137-
or (@line1 = parent::expr/following-sibling::*[1][not(self::ELSE)]/@line1)
137+
or (@line1 = parent::expr/following-sibling::*[not(self::COMMENT)][1][not(self::ELSE)]/@line1)
138138
)
139139
]")
140140

R/coalesce_linter.R

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,34 +46,34 @@
4646
coalesce_linter <- function() {
4747
braced_expr_cond <- "expr[1][OP-LEFT-BRACE and count(*) = 3]/expr"
4848
xpath <- glue("
49-
parent::expr[
49+
expr[expr[
5050
preceding-sibling::IF
5151
and (
5252
expr[2] = following-sibling::ELSE/following-sibling::expr
5353
or expr[2] = following-sibling::ELSE/following-sibling::{braced_expr_cond}
5454
or expr[2][LEFT_ASSIGN]/expr[1] = following-sibling::ELSE/following-sibling::expr
5555
or expr[2][LEFT_ASSIGN]/expr[1] = following-sibling::ELSE/following-sibling::{braced_expr_cond}
5656
)
57-
]
58-
/parent::expr
57+
]]
5958
|
60-
parent::expr[
61-
preceding-sibling::OP-EXCLAMATION
62-
and parent::expr/preceding-sibling::IF
63-
and parent::expr/following-sibling::ELSE
59+
self::*[expr[
60+
preceding-sibling::IF
61+
and following-sibling::ELSE
62+
and OP-EXCLAMATION
6463
and (
65-
expr[2] = parent::expr/following-sibling::expr[1]
66-
or expr[2] = parent::expr/following-sibling::{braced_expr_cond}
67-
or expr[2][LEFT_ASSIGN]/expr[1] = parent::expr/following-sibling::expr[1]
68-
or expr[2][LEFT_ASSIGN]/expr[1] = parent::expr/following-sibling::{braced_expr_cond}
64+
expr/expr[2] = following-sibling::expr[1]
65+
or expr/expr[2] = following-sibling::{braced_expr_cond}
66+
or expr/expr[2][LEFT_ASSIGN]/expr[1] = following-sibling::expr[1]
67+
or expr/expr[2][LEFT_ASSIGN]/expr[1] = following-sibling::{braced_expr_cond}
6968
)
70-
]
71-
/parent::expr
72-
/parent::expr
69+
]]
7370
")
7471

7572
Linter(linter_level = "expression", function(source_expression) {
76-
null_calls <- source_expression$xml_find_function_calls("is.null")
73+
null_calls <- xml_parent(xml_parent(xml_parent(
74+
source_expression$xml_find_function_calls("is.null")
75+
)))
76+
null_calls <- strip_comments_from_subtree(null_calls)
7777
bad_expr <- xml_find_all(null_calls, xpath)
7878
is_negation <- !is.na(xml_find_first(bad_expr, "expr/OP-EXCLAMATION"))
7979
observed <- ifelse(is_negation, "if (!is.null(x)) x else y", "if (is.null(x)) y else x")

R/empty_assignment_linter.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
empty_assignment_linter <- make_linter_from_xpath(
3434
# for some reason, the parent in the `=` case is <equal_assign>, not <expr>, hence parent::expr
3535
xpath = "
36-
//OP-LEFT-BRACE[following-sibling::*[1][self::OP-RIGHT-BRACE]]
36+
//OP-LEFT-BRACE[following-sibling::*[not(self::COMMENT)][1][self::OP-RIGHT-BRACE]]
3737
/parent::expr[
3838
preceding-sibling::LEFT_ASSIGN
3939
or preceding-sibling::EQ_ASSIGN

R/expect_comparison_linter.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ expect_comparison_linter <- function() {
6565
xml_calls <- source_expression$xml_find_function_calls("expect_true")
6666
bad_expr <- xml_find_all(xml_calls, xpath)
6767

68-
comparator <- xml_find_chr(bad_expr, "string(expr[2]/*[2])")
68+
comparator <- xml_find_chr(bad_expr, "string(expr[2]/*[not(self::COMMENT)][2])")
6969
expectation <- comparator_expectation_map[comparator]
7070
lint_message <- sprintf("%s(x, y) is better than expect_true(x %s y).", expectation, comparator)
7171
xml_nodes_to_lints(bad_expr, source_expression, lint_message = lint_message, type = "warning")

R/if_switch_linter.R

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,6 @@ if_switch_linter <- function(max_branch_lines = 0L, max_branch_expressions = 0L)
191191
# NB: IF AND {...} AND ELSE/... implies >= 3 equality conditions are present
192192
# .//expr/IF/...: the expr in `==` that's _not_ the STR_CONST
193193
# not(preceding::IF): prevent nested matches which might be incorrect globally
194-
# not(. != .): don't match if there are _any_ expr which _don't_ match the top
195-
# expr
196194
if_xpath <- glue("
197195
//IF
198196
/parent::expr[
@@ -203,21 +201,27 @@ if_switch_linter <- function(max_branch_lines = 0L, max_branch_expressions = 0L)
203201
and {equal_str_cond}
204202
and ELSE/following-sibling::expr[IF and {equal_str_cond}]
205203
]
206-
and not(
207-
.//expr/IF/following-sibling::{equal_str_cond}/expr[not(STR_CONST)]
208-
!= expr[1][EQ]/expr[not(STR_CONST)]
209-
)
210204
and not({ max_lines_cond })
211205
]
212206
")
213207

208+
# not(. != .): don't match if there are _any_ expr which _don't_ match the top expr
209+
equality_test_cond <- glue("self::*[
210+
.//expr/IF/following-sibling::{equal_str_cond}/expr[not(STR_CONST)]
211+
!= expr[1][EQ]/expr[not(STR_CONST)]
212+
]")
213+
214214
Linter(linter_level = "expression", function(source_expression) {
215215
xml <- source_expression$xml_parsed_content
216216

217217
bad_expr <- xml_find_all(xml, if_xpath)
218+
expr_all_equal <- is.na(xml_find_first(
219+
strip_comments_from_subtree(bad_expr),
220+
equality_test_cond
221+
))
218222

219223
lints <- xml_nodes_to_lints(
220-
bad_expr,
224+
bad_expr[expr_all_equal],
221225
source_expression = source_expression,
222226
lint_message = paste(
223227
"Prefer switch() statements over repeated if/else equality tests,",

R/implicit_assignment_linter.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ implicit_assignment_linter <- function(except = c("bquote", "expression", "expr"
9696
xpath <- glue("
9797
({assignments})
9898
/parent::expr[
99-
preceding-sibling::*[2][self::IF or self::WHILE]
99+
preceding-sibling::*[not(self::COMMENT)][2][self::IF or self::WHILE]
100100
or parent::forcond
101101
or preceding-sibling::expr/{xpath_exceptions}
102102
or parent::expr/*[1][self::OP-LEFT-PAREN]
@@ -108,7 +108,7 @@ implicit_assignment_linter <- function(except = c("bquote", "expression", "expr"
108108
}
109109
if (allow_scoped) {
110110
# force 2nd preceding to ensure we're in the loop condition, not the loop expression
111-
in_branch_cond <- "ancestor-or-self::expr[preceding-sibling::*[2][self::IF or self::WHILE]]"
111+
in_branch_cond <- "ancestor-or-self::expr[preceding-sibling::*[not(self::COMMENT)][2][self::IF or self::WHILE]]"
112112
xpath <- paste0(
113113
xpath,
114114
# _if_ we're in an IF/WHILE branch, lint if the assigned SYMBOL appears anywhere later on.

R/length_test_linter.R

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,14 @@ length_test_linter <- function() {
5151
Linter(linter_level = "expression", function(source_expression) {
5252
xml_calls <- source_expression$xml_find_function_calls(c("length", "nrow", "ncol", "NROW", "NCOL"))
5353
bad_expr <- xml_find_all(xml_calls, xpath)
54+
bad_expr <- strip_comments_from_subtree(bad_expr)
5455

5556
matched_function <- xp_call_name(bad_expr)
56-
expr_parts <- vapply(lapply(bad_expr, xml_find_all, "expr[2]/*"), xml_text, character(3L))
57+
expr_parts <- vapply(
58+
lapply(bad_expr, xml_find_all, "expr[2]/*[not(self::COMMENT)]"),
59+
xml_text,
60+
character(3L)
61+
)
5762
lint_message <- sprintf(
5863
"Checking the %s of a logical vector is likely a mistake. Did you mean `%s(%s) %s %s`?",
5964
matched_function, matched_function, expr_parts[1L, ], expr_parts[2L, ], expr_parts[3L, ]

R/object_usage_linter.R

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,24 @@ object_usage_linter <- function(interpret_glue = NULL, interpret_extensions = c(
6161
# NB: the repeated expr[2][FUNCTION] XPath has no performance impact, so the different direct assignment XPaths are
6262
# split for better readability, see PR#1197
6363
# TODO(#1106): use //[...] to capture assignments in more scopes
64-
xpath_function_assignment <- "
65-
expr[LEFT_ASSIGN or EQ_ASSIGN]/expr[2][FUNCTION or OP-LAMBDA]
66-
| expr_or_assign_or_help[EQ_ASSIGN]/expr[2][FUNCTION or OP-LAMBDA]
67-
| equal_assign[EQ_ASSIGN]/expr[2][FUNCTION or OP-LAMBDA]
68-
| //SYMBOL_FUNCTION_CALL[text() = 'assign']/parent::expr/following-sibling::expr[2][FUNCTION or OP-LAMBDA]
69-
| //SYMBOL_FUNCTION_CALL[text() = 'setMethod']/parent::expr/following-sibling::expr[3][FUNCTION or OP-LAMBDA]
70-
"
64+
fun_node <- "FUNCTION or OP-LAMBDA"
65+
xpath_function_assignment <- glue("
66+
expr[LEFT_ASSIGN or EQ_ASSIGN]/expr[2][{fun_node}]
67+
| expr_or_assign_or_help[EQ_ASSIGN]/expr[2][{fun_node}]
68+
| equal_assign[EQ_ASSIGN]/expr[2][{fun_node}]
69+
| //SYMBOL_FUNCTION_CALL[text() = 'assign']/parent::expr/following-sibling::expr[2][{fun_node}]
70+
| //SYMBOL_FUNCTION_CALL[text() = 'setMethod']/parent::expr/following-sibling::expr[3][{fun_node}]
71+
")
72+
73+
# code like:
74+
# foo <- \ #comment
75+
# (x) x
76+
# is technically valid, but won't parse unless the lambda is in a bigger expression (here '<-').
77+
# the same doesn't apply to 'function', which is acknowledged as "not worth a breaking change to fix":
78+
# https://bugs.r-project.org/show_bug.cgi?id=18924. If we find such code (which has only ever
79+
# arisen in content fuzzing where we inject comments at random to the AST), we have to avoid parsing
80+
# it as a standalone expression.
81+
xpath_unsafe_lambda <- "OP-LAMBDA[@line1 = following-sibling::*[1][self::COMMENT]/@line1]"
7182

7283
# not all instances of linted symbols are potential sources for the observed violations -- see #1914
7384
symbol_exclude_cond <- "preceding-sibling::OP-DOLLAR or preceding-sibling::OP-AT or ancestor::expr[OP-TILDE]"
@@ -100,7 +111,9 @@ object_usage_linter <- function(interpret_glue = NULL, interpret_extensions = c(
100111
fun_assignments <- xml_find_all(xml, xpath_function_assignment)
101112

102113
lapply(fun_assignments, function(fun_assignment) {
103-
code <- get_content(lines = source_expression$content, fun_assignment)
114+
# this will mess with the source line numbers. but I don't think anybody cares.
115+
needs_braces <- !is.na(xml_find_first(fun_assignment, xpath_unsafe_lambda))
116+
code <- get_content(lines = source_expression$content, fun_assignment, needs_braces = needs_braces)
104117
fun <- try_silently(eval(
105118
envir = env,
106119
parse(
@@ -190,8 +203,8 @@ get_assignment_symbols <- function(xml) {
190203
expr[RIGHT_ASSIGN]/expr[2]/SYMBOL[1] |
191204
equal_assign/expr[1]/SYMBOL[1] |
192205
expr_or_assign_or_help/expr[1]/SYMBOL[1] |
193-
expr[expr[1][SYMBOL_FUNCTION_CALL/text()='assign']]/expr[2]/* |
194-
expr[expr[1][SYMBOL_FUNCTION_CALL/text()='setMethod']]/expr[2]/*
206+
expr[expr[1][SYMBOL_FUNCTION_CALL/text() = 'assign']]/expr[2]/* |
207+
expr[expr[1][SYMBOL_FUNCTION_CALL/text() = 'setMethod']]/expr[2]/*
195208
"
196209
))
197210
}

R/redundant_equals_linter.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ redundant_equals_linter <- function() {
5858
xml <- source_expression$xml_parsed_content
5959

6060
bad_expr <- xml_find_all(xml, xpath)
61-
op <- xml_text(xml_find_first(bad_expr, "*[2]"))
61+
op <- xml_text(xml_find_first(bad_expr, "*[not(self::COMMENT)][2]"))
6262

6363
xml_nodes_to_lints(
6464
bad_expr,

R/regex_subset_linter.R

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,23 @@
4747
#' @seealso [linters] for a complete list of linters available in lintr.
4848
#' @export
4949
regex_subset_linter <- function() {
50-
# parent::expr for LEFT_ASSIGN and RIGHT_ASSIGN, but, strangely,
51-
# parent::equal_assign for EQ_ASSIGN. So just use * as a catchall.
52-
# See https://www.w3.org/TR/1999/REC-xpath-19991116/#booleans;
53-
# equality of nodes is based on the string value of the nodes, which
54-
# is basically what we need, i.e., whatever expression comes in
55-
# <expr>[grepl(pattern, <expr>)] matches exactly, e.g. names(x)[grepl(ptn, names(x))].
5650
xpath_fmt <- "
57-
parent::expr[
58-
parent::expr[
51+
self::*[
52+
not(LEFT_ASSIGN or EQ_ASSIGN or RIGHT_ASSIGN)
53+
]
54+
/expr[
5955
OP-LEFT-BRACKET
60-
and not(parent::*[LEFT_ASSIGN or EQ_ASSIGN or RIGHT_ASSIGN])
56+
and expr[1] = expr/expr[position() = {arg_pos} ]
6157
]
62-
and expr[position() = {arg_pos} ] = parent::expr/expr[1]
63-
]"
58+
"
6459
grep_xpath <- glue(xpath_fmt, arg_pos = 3L)
6560
stringr_xpath <- glue(xpath_fmt, arg_pos = 2L)
6661

6762
Linter(linter_level = "expression", function(source_expression) {
68-
grep_calls <- source_expression$xml_find_function_calls(c("grepl", "grep"))
63+
grep_calls <- xml_parent(xml_parent(xml_parent(
64+
source_expression$xml_find_function_calls(c("grepl", "grep"))
65+
)))
66+
grep_calls <- strip_comments_from_subtree(grep_calls)
6967
grep_expr <- xml_find_all(grep_calls, grep_xpath)
7068

7169
grep_lints <- xml_nodes_to_lints(
@@ -78,7 +76,10 @@ regex_subset_linter <- function() {
7876
type = "warning"
7977
)
8078

81-
stringr_calls <- source_expression$xml_find_function_calls(c("str_detect", "str_which"))
79+
stringr_calls <- xml_parent(xml_parent(xml_parent(
80+
source_expression$xml_find_function_calls(c("str_detect", "str_which"))
81+
)))
82+
stringr_calls <- strip_comments_from_subtree(stringr_calls)
8283
stringr_expr <- xml_find_all(stringr_calls, stringr_xpath)
8384

8485
stringr_lints <- xml_nodes_to_lints(

0 commit comments

Comments
 (0)