Skip to content

Commit c21600a

Browse files
fixed_regex_linter is pipe-aware (#2094)
* add tests to get working * fixed_regex_linter pipe-aware * merge NEWS items * xpath style * test all pipes --------- Co-authored-by: AshesITR <[email protected]>
1 parent 1c33794 commit c21600a

File tree

3 files changed

+51
-3
lines changed

3 files changed

+51
-3
lines changed

NEWS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
## New and improved features
99

1010
* New exclusion sentinel `# nolint next` to signify the next line should skip linting (#1791, @MichaelChirico). The usual rules apply for excluding specific linters, e.g. `# nolint next: assignment_linter.`. The exact string used to match a subsequent-line exclusion is controlled by the `exclude_next` config entry or R option `"lintr.exclude_next"`.
11+
* `fixed_regex_linter()`
12+
+ Is pipe-aware, in particular removing false positives arong piping into {stringr} functions like `x |> str_replace(fixed("a"), "b")` (#1811, @MichaelChirico).
13+
+ Gains an option `allow_unescaped` (default `FALSE`) to toggle linting regexes not requiring any escapes or character classes (#1689, @MichaelChirico). Thus `fixed_regex_linter(allow_unescaped = TRUE)` would lint on `grepl("[$]", x)` but not on `grepl("a", x)` since the latter does not use any regex special characters.
1114
* Linters with logic around the magrittr pipe `%>%` consistently apply it to the other pipes `%!>%`, `%T>%`, `%<>%` (and possibly `%$%`) where appropriate (#2008, @MichaelChirico).
1215
+ `brace_linter()`
1316
+ `pipe_call_linter()`
@@ -27,7 +30,6 @@
2730
* `conjunct_test_linter()` also lints usage like `dplyr::filter(x, A & B)` in favor of using `dplyr::filter(x, A, B)` (part of #884, @MichaelChirico).
2831
* `sort_linter()` checks for code like `x == sort(x)` which is better served by using the function `is.unsorted()` (part of #884, @MichaelChirico).
2932
* `paste_linter()` gains detection for file paths that are better constructed with `file.path()`, e.g. `paste0(dir, "/", file)` would be better as `file.path(dir, file)` (part of #884, @MichaelChirico).
30-
* `fixed_regex_linter()` gains an option `allow_unescaped` (default `FALSE`) to toggle linting regexes not requiring any escapes or character classes (#1689, @MichaelChirico). Thus `fixed_regex_linter(allow_unescaped = TRUE)` would lint on `grepl("[$]", x)` but not on `grepl("a", x)` since the latter does not use any regex special characters.
3133

3234
### New linters
3335

R/fixed_regex_linter.R

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ fixed_regex_linter <- function(allow_unescaped = FALSE) {
9797
"str_view", "str_view_all", "str_which"
9898
))
9999

100+
pipes <- setdiff(magrittr_pipes, c("%$%", "%T>%"))
101+
in_pipe_cond <- glue("
102+
parent::expr/preceding-sibling::SPECIAL[{ xp_text_in_table(pipes) }]
103+
| parent::expr/preceding-sibling::PIPE
104+
")
105+
100106
# NB: strsplit doesn't have an ignore.case argument
101107
# NB: we intentionally exclude cases like gsub(x, c("a" = "b")), where "b" is fixed
102108
xpath <- glue("
@@ -107,7 +113,16 @@ fixed_regex_linter <- function(allow_unescaped = FALSE) {
107113
and following-sibling::expr[1][NUM_CONST[text() = 'TRUE'] or SYMBOL[text() = 'T']]
108114
])
109115
]
110-
/following-sibling::expr[1][STR_CONST and not(EQ_SUB)]
116+
/following-sibling::expr[
117+
(
118+
position() = 1
119+
and STR_CONST
120+
and not(EQ_SUB)
121+
and not({ in_pipe_cond })
122+
) or (
123+
preceding-sibling::*[2][self::SYMBOL_SUB/text() = 'pattern']
124+
)
125+
]
111126
|
112127
//SYMBOL_FUNCTION_CALL[ {pos_2_regex_funs} ]
113128
/parent::expr[
@@ -116,7 +131,11 @@ fixed_regex_linter <- function(allow_unescaped = FALSE) {
116131
and following-sibling::expr[1][NUM_CONST[text() = 'TRUE'] or SYMBOL[text() = 'T']]
117132
])
118133
]
119-
/following-sibling::expr[2][STR_CONST and not(EQ_SUB)]
134+
/following-sibling::expr[
135+
position() = 2 - count({ in_pipe_cond })
136+
and STR_CONST
137+
and not(EQ_SUB)
138+
]
120139
")
121140

122141
Linter(function(source_expression) {

tests/testthat/test-fixed_regex_linter.R

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,30 @@ test_that("'unescaped' regex can optionally be skipped", {
353353
expect_lint("str_detect(x, 'a')", NULL, linter)
354354
expect_lint("grepl('[$]', x)", rex::rex('Here, you can use "$"'), linter)
355355
})
356+
357+
local({
358+
pipes <- pipes(exclude = c("%$%", "%T>%"))
359+
patrick::with_parameters_test_that(
360+
"linter is pipe-aware",
361+
{
362+
linter <- fixed_regex_linter()
363+
lint_msg <- "This regular expression is static"
364+
365+
expect_lint(paste("x", pipe, "grepl(pattern = 'a')"), lint_msg, linter)
366+
expect_lint(paste("x", pipe, "grepl(pattern = '^a')"), NULL, linter)
367+
expect_lint(paste("x", pipe, "grepl(pattern = 'a', fixed = TRUE)"), NULL, linter)
368+
expect_lint(paste("x", pipe, "str_detect('a')"), lint_msg, linter)
369+
expect_lint(paste("x", pipe, "str_detect('^a')"), NULL, linter)
370+
expect_lint(paste("x", pipe, "str_detect(fixed('a'))"), NULL, linter)
371+
372+
expect_lint(paste("x", pipe, "gsub(pattern = 'a', replacement = '')"), lint_msg, linter)
373+
expect_lint(paste("x", pipe, "gsub(pattern = '^a', replacement = '')"), NULL, linter)
374+
expect_lint(paste("x", pipe, "gsub(pattern = 'a', replacement = '', fixed = TRUE)"), NULL, linter)
375+
expect_lint(paste("x", pipe, "str_replace('a', '')"), lint_msg, linter)
376+
expect_lint(paste("x", pipe, "str_replace('^a', '')"), NULL, linter)
377+
expect_lint(paste("x", pipe, "str_replace(fixed('a'), '')"), NULL, linter)
378+
},
379+
pipe = pipes,
380+
.test_name = names(pipes)
381+
)
382+
})

0 commit comments

Comments
 (0)