Skip to content

Commit

Permalink
New which_grepl_linter (#2281)
Browse files Browse the repository at this point in the history
* New which_grepl_linter

* correct test to use '|'

* examples

* new "regex" linter tag

* forgot to check this in

* wording

* fix doc

---------

Co-authored-by: Indrajeet Patil <[email protected]>
  • Loading branch information
MichaelChirico and IndrajeetPatil authored Nov 18, 2023
1 parent 1d84cae commit 6634664
Show file tree
Hide file tree
Showing 16 changed files with 138 additions and 12 deletions.
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ Collate:
'unused_import_linter.R'
'use_lintr.R'
'vector_logic_linter.R'
'which_grepl_linter.R'
'whitespace_linter.R'
'with.R'
'with_id.R'
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export(unreachable_code_linter)
export(unused_import_linter)
export(use_lintr)
export(vector_logic_linter)
export(which_grepl_linter)
export(whitespace_linter)
export(with_defaults)
export(with_id)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* `comparison_negation_linter()` for discouraging negated comparisons when a direct negation is preferable, e.g. `!(x == y)` could be `x != y` (part of #884, @MichaelChirico).
* `nzchar_linter()` for encouraging `nzchar()` to test for empty strings, e.g. `nchar(x) > 0` can be `nzchar(x)` (part of #884, @MichaelChirico).
* `terminal_close_linter()` for discouraging using `close()` to end functions (part of #884, @MichaelChirico). Such usages are not robust to errors, where `close()` will not be run as intended. Put `close()` in an `on.exit()` hook, or use {withr} to manage connections with proper cleanup.
* `which_grepl_linter()` for discouraging `which(grepl(ptn, x))` in favor of directly using `grep(ptn, x)` (part of #884, @MichaelChirico).
* `list_comparison_linter()` for discouraging comparisons on the output of `lapply()`, e.g. `lapply(x, sum) > 10` (part of #884, @MichaelChirico).
* `print_linter()` for discouraging usage of `print()` on string literals like `print("Reached here")` or `print(paste("Found", nrow(DF), "rows."))` (#1894, @MichaelChirico).

Expand Down
8 changes: 8 additions & 0 deletions R/linter_tag_docs.R
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,11 @@ NULL
#' - <https://testthat.r-lib.org>
#' - <https://r-pkgs.org/testing-basics.html>
NULL

#' Regular expression linters
#' @name regex_linters
#' @description
#' Linters that examine the usage of regular expressions and functions executing them in user code.
#' @evalRd rd_linters("regex")
#' @seealso [linters] for a complete list of linters available in lintr.
NULL
30 changes: 30 additions & 0 deletions R/which_grepl_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#' Require usage of grep over which(grepl(.))
#'
#' `which(grepl(pattern, x))` is the same as `grep(pattern, x)`, but harder
#' to read and requires two passes over the vector.
#'
#' @examples
#' # will produce lints
#' lint(
#' text = "which(grepl('^a', x))",
#' linters = which_grepl_linter()
#' )
#'
#' # okay
#' lint(
#' text = "which(grepl('^a', x) | grepl('^b', x))",
#' linters = which_grepl_linter()
#' )
#'
#' @evalRd rd_tags("which_grepl_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
which_grepl_linter <- make_linter_from_xpath(
xpath = "
//SYMBOL_FUNCTION_CALL[text() = 'grepl']
/parent::expr
/parent::expr
/parent::expr[expr/SYMBOL_FUNCTION_CALL[text() = 'which']]
",
lint_message = "grep(pattern, x) is better than which(grepl(pattern, x))."
)
7 changes: 4 additions & 3 deletions inst/lintr/linters.csv
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ expect_s4_class_linter,package_development best_practices pkg_testthat
expect_true_false_linter,package_development best_practices readability pkg_testthat
expect_type_linter,package_development best_practices pkg_testthat
extraction_operator_linter,style best_practices
fixed_regex_linter,best_practices readability efficiency configurable
fixed_regex_linter,best_practices readability efficiency configurable regex
for_loop_index_linter,best_practices readability robustness
function_argument_linter,style consistency best_practices
function_left_parentheses_linter,style readability default
Expand Down Expand Up @@ -76,7 +76,7 @@ print_linter,best_practices consistency
quotes_linter,style consistency readability default configurable
redundant_equals_linter,best_practices readability efficiency common_mistakes
redundant_ifelse_linter,best_practices efficiency consistency configurable
regex_subset_linter,best_practices efficiency
regex_subset_linter,best_practices efficiency regex
repeat_linter,style readability
routine_registration_linter,best_practices efficiency robustness
sample_int_linter,efficiency readability robustness
Expand All @@ -90,7 +90,7 @@ spaces_inside_linter,style readability default
spaces_left_parentheses_linter,style readability default
sprintf_linter,correctness common_mistakes
stopifnot_all_linter,readability best_practices
string_boundary_linter,readability efficiency configurable
string_boundary_linter,readability efficiency configurable regex
strings_as_factors_linter,robustness
system_file_linter,consistency readability best_practices
T_and_F_symbol_linter,style readability robustness consistency best_practices default
Expand All @@ -108,5 +108,6 @@ unneeded_concatenation_linter,style readability efficiency configurable deprecat
unreachable_code_linter,best_practices readability
unused_import_linter,best_practices common_mistakes configurable executing
vector_logic_linter,default efficiency best_practices
which_grepl_linter,readability efficiency consistency regex
whitespace_linter,style consistency default
yoda_test_linter,package_development best_practices readability pkg_testthat
1 change: 1 addition & 0 deletions man/consistency_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/efficiency_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/fixed_regex_linter.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 8 additions & 6 deletions man/linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/readability_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions man/regex_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/regex_subset_linter.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/string_boundary_linter.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions man/which_grepl_linter.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions tests/testthat/test-which_grepl_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
test_that("which_grepl_linter skips allowed usages", {
# this _could_ be combined as p1|p2, but often it's cleaner to read this way
expect_lint("which(grepl(p1, x) | grepl(p2, x))", NULL, which_grepl_linter())
})

test_that("which_grepl_linter blocks simple disallowed usages", {
linter <- which_grepl_linter()
lint_msg <- rex::rex("grep(pattern, x) is better than which(grepl(pattern, x)).")

expect_lint("which(grepl('^a', x))", lint_msg, linter)
# options also don't matter (grep has more arguments: value, invert)
expect_lint("which(grepl('^a', x, perl = TRUE, fixed = TRUE))", lint_msg, linter)

expect_lint(
trim_some('{
which(x)
grepl(y)
which(grepl("pt1", x))
which(grepl("pt2", y))
}'),
list(
list(lint_msg, line_number = 4L, column_number = 3L),
list(lint_msg, line_number = 5L, column_number = 3L)
),
linter
)
})

0 comments on commit 6634664

Please sign in to comment.