Skip to content

Commit d6267ac

Browse files
Improve robustness to comments in literal_coercion_linter (#2895)
1 parent d3b0e34 commit d6267ac

File tree

3 files changed

+80
-19
lines changed

3 files changed

+80
-19
lines changed

R/literal_coercion_linter.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ literal_coercion_linter <- function() {
6161
not(OP-DOLLAR or OP-AT)
6262
and (
6363
NUM_CONST[not(contains(translate(text(), 'E', 'e'), 'e'))]
64-
or STR_CONST[not(following-sibling::*[1][self::EQ_SUB])]
64+
or STR_CONST[not(following-sibling::*[not(self::COMMENT)][1][self::EQ_SUB])]
6565
)
6666
"
6767
xpath <- glue("
@@ -89,6 +89,7 @@ literal_coercion_linter <- function() {
8989
)
9090
# nocov end
9191
} else {
92+
bad_expr <- strip_comments_from_subtree(bad_expr)
9293
# duplicate, unless we add 'rlang::' and it wasn't there originally
9394
coercion_str <- report_str <- xml_text(bad_expr)
9495
if (any(is_rlang_coercer) && !("package:rlang" %in% search())) {

R/xml_utils.R

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,28 @@ xml2lang <- function(x) {
1212
str2lang(paste(xml_text(x_strip_comments), collapse = " "))
1313
}
1414

15+
# TODO(r-lib/xml2#341): Use xml_clone() instead.
16+
clone_xml_ <- function(x) {
17+
tmp_doc <- tempfile()
18+
on.exit(unlink(tmp_doc))
19+
20+
doc <- xml2::xml_new_root("root")
21+
for (ii in seq_along(x)) {
22+
xml2::write_xml(x[[ii]], tmp_doc)
23+
xml2::xml_add_child(doc, xml2::read_xml(tmp_doc))
24+
}
25+
xml_find_all(doc, "*")
26+
}
27+
28+
# caveat: whether this is a copy or not is inconsistent. assume the output is read-only!
29+
strip_comments_from_subtree <- function(expr) {
30+
if (length(xml_find_first(expr, ".//COMMENT")) == 0L) {
31+
return(expr)
32+
}
33+
expr <- clone_xml_(expr)
34+
for (comment in xml_find_all(expr, ".//COMMENT")) xml2::xml_remove(comment)
35+
expr
36+
}
1537

1638
safe_parse_to_xml <- function(parsed_content) {
1739
if (is.null(parsed_content)) {

tests/testthat/test-literal_coercion_linter.R

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,50 @@ test_that("literal_coercion_linter skips allowed usages", {
22
linter <- literal_coercion_linter()
33

44
# naive xpath includes the "_f0" here as a literal
5-
expect_lint('as.numeric(x$"_f0")', NULL, linter)
6-
expect_lint('as.numeric(x@"_f0")', NULL, linter)
5+
expect_no_lint('as.numeric(x$"_f0")', linter)
6+
expect_no_lint('as.numeric(x@"_f0")', linter)
77
# only examine the first method for as.<type> methods
8-
expect_lint("as.character(as.Date(x), '%Y%m%d')", NULL, linter)
8+
expect_no_lint("as.character(as.Date(x), '%Y%m%d')", linter)
99

1010
# we are as yet agnostic on whether to prefer literals over coerced vectors
11-
expect_lint("as.integer(c(1, 2, 3))", NULL, linter)
11+
expect_no_lint("as.integer(c(1, 2, 3))", linter)
1212
# even more ambiguous for character vectors like here, where quotes are much
1313
# more awkward to type than a sequence of numbers
14-
expect_lint("as.character(c(1, 2, 3))", NULL, linter)
14+
expect_no_lint("as.character(c(1, 2, 3))", linter)
1515
# not possible to declare raw literals
16-
expect_lint("as.raw(c(1, 2, 3))", NULL, linter)
16+
expect_no_lint("as.raw(c(1, 2, 3))", linter)
1717
# also not taking a stand on as.complex(0) vs. 0 + 0i
18-
expect_lint("as.complex(0)", NULL, linter)
18+
expect_no_lint("as.complex(0)", linter)
1919
# ditto for as.integer(1e6) vs. 1000000L
20-
expect_lint("as.integer(1e6)", NULL, linter)
20+
expect_no_lint("as.integer(1e6)", linter)
2121
# ditto for as.numeric(1:3) vs. c(1, 2, 3)
22-
expect_lint("as.numeric(1:3)", NULL, linter)
22+
expect_no_lint("as.numeric(1:3)", linter)
2323
})
2424

2525
test_that("literal_coercion_linter skips allowed rlang usages", {
2626
linter <- literal_coercion_linter()
2727

28-
expect_lint("int(1, 2.0, 3)", NULL, linter)
29-
expect_lint("chr('e', 'ab', 'xyz')", NULL, linter)
30-
expect_lint("lgl(0, 1)", NULL, linter)
31-
expect_lint("lgl(0L, 1)", NULL, linter)
32-
expect_lint("dbl(1.2, 1e5, 3L, 2E4)", NULL, linter)
28+
expect_no_lint("int(1, 2.0, 3)", linter)
29+
expect_no_lint("chr('e', 'ab', 'xyz')", linter)
30+
expect_no_lint("lgl(0, 1)", linter)
31+
expect_no_lint("lgl(0L, 1)", linter)
32+
expect_no_lint("dbl(1.2, 1e5, 3L, 2E4)", linter)
3333
# make sure using namespace (`rlang::`) doesn't create problems
34-
expect_lint("rlang::int(1, 2, 3)", NULL, linter)
34+
expect_no_lint("rlang::int(1, 2, 3)", linter)
3535
# even if scalar, carve out exceptions for the following
36-
expect_lint("int(1.0e6)", NULL, linter)
36+
expect_no_lint("int(1.0e6)", linter)
3737
})
3838

3939
test_that("literal_coercion_linter skips quoted keyword arguments", {
40-
expect_lint("as.numeric(foo('a' = 1))", NULL, literal_coercion_linter())
40+
linter <- literal_coercion_linter()
41+
expect_no_lint("as.numeric(foo('a' = 1))", linter)
42+
expect_no_lint(
43+
trim_some("
44+
as.numeric(foo('a' # comment
45+
= 1))
46+
"),
47+
linter
48+
)
4149
})
4250

4351
test_that("no warnings surfaced by running coercion", {
@@ -50,6 +58,18 @@ test_that("no warnings surfaced by running coercion", {
5058
expect_no_warning(
5159
expect_lint("as.integer(2147483648)", "Use NA_integer_", linter)
5260
)
61+
62+
expect_no_warning(
63+
expect_lint(
64+
trim_some("
65+
as.double(
66+
NA # comment
67+
)
68+
"),
69+
"Use NA_real_",
70+
linter
71+
)
72+
)
5373
})
5474

5575
skip_if_not_installed("tibble")
@@ -81,6 +101,7 @@ patrick::with_parameters_test_that(
81101

82102
skip_if_not_installed("rlang")
83103
test_that("multiple lints return custom messages", {
104+
linter <- literal_coercion_linter()
84105
expect_lint(
85106
trim_some("{
86107
as.integer(1)
@@ -90,7 +111,24 @@ test_that("multiple lints return custom messages", {
90111
list(rex::rex("Use 1L instead of as.integer(1)"), line_number = 2L),
91112
list(rex::rex("Use TRUE instead of lgl(1L)"), line_number = 3L)
92113
),
93-
literal_coercion_linter()
114+
linter
115+
)
116+
117+
# also ensure comment remove logic works across several lints
118+
expect_lint(
119+
trim_some("{
120+
as.integer( # comment
121+
1 # comment
122+
) # comment
123+
lgl( # comment
124+
1L # comment
125+
) # comment
126+
}"),
127+
list(
128+
list(rex::rex("Use 1L instead of as.integer(1)"), line_number = 2L),
129+
list(rex::rex("Use TRUE instead of lgl(1L)"), line_number = 5L)
130+
),
131+
linter
94132
)
95133
})
96134

0 commit comments

Comments
 (0)