Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New boolean_arithmetic_linter #1579

Merged
merged 6 commits into from
Oct 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Collate:
'any_is_na_linter.R'
'assignment_linter.R'
'backport_linter.R'
'boolean_arithmetic_linter.R'
'brace_linter.R'
'cache.R'
'class_equals_linter.R'
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export(assignment_linter)
export(available_linters)
export(available_tags)
export(backport_linter)
export(boolean_arithmetic_linter)
export(brace_linter)
export(checkstyle_output)
export(class_equals_linter)
Expand Down
6 changes: 5 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@

* `unnecessary_lambda_linter()`: detect unnecessary lambdas (anonymous functions), e.g.
`lapply(x, function(xi) sum(xi))` can be `lapply(x, sum)` and `purrr::map(x, ~quantile(.x, 0.75, na.rm = TRUE))`
can be `purrr::map(x, quantile, 0.75, na.rm = TRUE)`. Naming `probs = 0.75` can further improve readability.
can be `purrr::map(x, quantile, 0.75, na.rm = TRUE)`. Naming `probs = 0.75` can further improve readability (#1531, @MichaelChirico).

* `redundant_equals_linter()` for redundant comparisons to `TRUE` or `FALSE` like `is_treatment == TRUE` (#1500, @MichaelChirico)
* `lengths_linter()` for encouraging usage of `lengths(x)` instead of `sapply(x, length)` (and similar)

* `function_return_linter()` for handling issues in function `return()` statements. Currently handles assignments within the `return()`
clause, e.g. `return(x <- foo())` (@MichaelChirico)

* `boolean_arithmetic_linter()` for identifying places where logical aggregations are more appropriate, e.g.
`length(which(x == y)) == 0` is the same as `!any(x == y)` or even `all(x != y)` (@MichaelChirico)

# lintr 3.0.1

* Skip multi-byte tests in non UTF-8 locales (#1504)
Expand Down
57 changes: 57 additions & 0 deletions R/boolean_arithmetic_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#' Require usage of boolean operators over equivalent arithmetic
#'
#' `length(which(x == y)) == 0` is the same as `!any(x == y)`, but the latter
#' is more readable and more efficient.
#'
#' @evalRd rd_tags("boolean_arithmetic_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
boolean_arithmetic_linter <- function() {
# TODO(#1580): sum() cases x %in% y, A [&|] B, !A, is.na/is.nan/is.finite/is.infinite/is.element
# TODO(#1581): extend to include all()-alike expressions
zero_expr <-
"(EQ or NE or GT or LE) and expr[NUM_CONST[text() = '0' or text() = '0L']]"
one_expr <-
"(LT or GE) and expr[NUM_CONST[text() = '1' or text() = '1L']]"
length_xpath <- glue::glue("
//SYMBOL_FUNCTION_CALL[text() = 'which' or text() = 'grep']
/parent::expr
/parent::expr
/parent::expr[
expr[SYMBOL_FUNCTION_CALL[text() = 'length']]
and parent::expr[ ({zero_expr}) or ({one_expr})]
]
")
sum_xpath <- glue::glue("
//SYMBOL_FUNCTION_CALL[text() = 'sum']
/parent::expr
/parent::expr[
expr[
expr[SYMBOL_FUNCTION_CALL[text() = 'grepl']]
or (EQ or NE or GT or LT or GE or LE)
] and parent::expr[ ({zero_expr}) or ({one_expr})]
]
")
any_xpath <- paste(length_xpath, "|", sum_xpath)

Linter(function(source_expression) {
if (!is_lint_level(source_expression, "expression")) {
return(list())
}

xml <- source_expression$xml_parsed_content

any_expr <- xml2::xml_find_all(xml, any_xpath)

xml_nodes_to_lints(
any_expr,
source_expression = source_expression,
# TODO(michaelchirico): customize this?
lint_message = paste(
"Use any() to express logical aggregations.",
"For example, replace length(which(x == y)) == 0 with !any(x == y)."
),
type = "warning"
)
})
}
1 change: 1 addition & 0 deletions inst/lintr/linters.csv
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ any_duplicated_linter,efficiency best_practices
any_is_na_linter,efficiency best_practices
assignment_linter,style consistency default
backport_linter,robustness configurable package_development
boolean_arithmetic_linter,efficiency best_practices readability
brace_linter,style readability default configurable
class_equals_linter,best_practices robustness consistency
closed_curly_linter,style readability deprecated configurable
Expand Down
1 change: 1 addition & 0 deletions man/best_practices_linters.Rd

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

18 changes: 18 additions & 0 deletions man/boolean_arithmetic_linter.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.

7 changes: 4 additions & 3 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 tests/testthat/test-boolean_arithmetic_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
test_that("boolean_arithmetic_linter requires use of any() or !any()", {
linter <- boolean_arithmetic_linter()
lint_msg <- rex::rex("Use any() to express logical aggregations.")

expect_lint("length(which(x == y)) == 0", lint_msg, linter)
# anything passed to which() can be assumed to be logical
expect_lint("length(which(is_treatment)) == 0L", lint_msg, linter)
# regex version
expect_lint("length(grep(pattern, x)) == 0", lint_msg, linter)
# sum version
expect_lint("sum(x == y) == 0L", lint_msg, linter)
expect_lint("sum(grepl(pattern, x)) == 0", lint_msg, linter)

# non-== comparisons
expect_lint("length(which(x == y)) > 0L", lint_msg, linter)
expect_lint("length(which(is_treatment)) < 1", lint_msg, linter)
expect_lint("length(grep(pattern, x)) >= 1L", lint_msg, linter)
expect_lint("sum(x == y) != 0", lint_msg, linter)
expect_lint("sum(grepl(pattern, x)) > 0L", lint_msg, linter)
})