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

expect_null_linter #887

Merged
merged 36 commits into from
Mar 12, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
43778a6
New expect_null_linter
MichaelChirico Nov 15, 2021
2b6c53b
NEWS
MichaelChirico Nov 15, 2021
4da6666
add tests
MichaelChirico Nov 15, 2021
10f87e5
add xpath utilities
MichaelChirico Nov 15, 2021
1bf7d38
roxygenize()
MichaelChirico Nov 15, 2021
7ae8df4
ignore nested .lintr configs
MichaelChirico Nov 15, 2021
842c15b
remove unneeded param from docs
MichaelChirico Nov 15, 2021
4696d5b
functionalize usage in tests
MichaelChirico Nov 15, 2021
5bc7b4b
remove unused arg
MichaelChirico Nov 15, 2021
b007d37
redocument
MichaelChirico Nov 15, 2021
e324084
remove @title
MichaelChirico Nov 15, 2021
bf8f726
revert
MichaelChirico Nov 15, 2021
18810c0
Merge branch 'master' into expect-null
MichaelChirico Nov 16, 2021
29a9d4c
Merge branch 'master' into expect-null
MichaelChirico Mar 11, 2022
719e3c7
Merge branch 'expect-null' of github.com:r-lib/lintr into expect-null
MichaelChirico Mar 11, 2022
76f1e69
remove google_linters page
MichaelChirico Mar 11, 2022
6f81ae2
switch to standalone docs
MichaelChirico Mar 11, 2022
c03b606
tweak NEWS
MichaelChirico Mar 11, 2022
4f90533
markdown format code
MichaelChirico Mar 11, 2022
5fce64c
add doc page for new tag
MichaelChirico Mar 11, 2022
b736aab
Merge branch 'master' into expect-null
MichaelChirico Mar 11, 2022
0df3979
add package_development to backport_linter and package_hooks_linter
MichaelChirico Mar 12, 2022
c15b51d
Merge branch 'expect-null' of github.com:r-lib/lintr into expect-null
MichaelChirico Mar 12, 2022
1dbdbe8
refactor to use a literal xpath
MichaelChirico Mar 12, 2022
d102c7e
another false positive test case
MichaelChirico Mar 12, 2022
a01e708
also lint on expect_equal(NULL, x)
MichaelChirico Mar 12, 2022
c04d24d
customize the lint message to the actual usage
MichaelChirico Mar 12, 2022
8712d9f
qualify xml2 namespace
MichaelChirico Mar 12, 2022
86496ab
pick up glue dep
MichaelChirico Mar 12, 2022
797e26b
manually add glue to deps
MichaelChirico Mar 12, 2022
1c5a97d
extra blank line
MichaelChirico Mar 12, 2022
fb2e6ac
remove glue
MichaelChirico Mar 12, 2022
06a6291
glue from DESC
MichaelChirico Mar 12, 2022
a32b298
glue from ns
MichaelChirico Mar 12, 2022
1a270de
glue from roxy
MichaelChirico Mar 12, 2022
6b6df30
manually revert collate order
MichaelChirico Mar 12, 2022
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
2 changes: 1 addition & 1 deletion .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
^\.Rproj\.user$
^\.idea$
^\.dev$
^\.lintr$
\.lintr$
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
^\.lintr_new$
^wercker\.yml$
^[^/]+.Rmd$
Expand Down
3 changes: 3 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ Collate:
'equals_na_linter.R'
'exclude.R'
'expect_lint.R'
'expect_null_linter.R'
'extract.R'
'extraction_operator_linter.R'
'function_left_parentheses.R'
'get_source_expressions.R'
'google_linters.R'
'ids_with_token.R'
'implicit_integer_linter.R'
'infix_spaces_linter.R'
Expand Down Expand Up @@ -98,4 +100,5 @@ Collate:
'undesirable_operator_linter.R'
'unneeded_concatenation_linter.R'
'with_id.R'
'xp_utils.R'
'zzz.R'
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export(duplicate_argument_linter)
export(equals_na_linter)
export(expect_lint)
export(expect_lint_free)
export(expect_null_linter)
export(extraction_operator_linter)
export(function_left_parentheses_linter)
export(get_source_expressions)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ function calls. (#850, #851, @renkun-ken)
* Several optional `Imported` packages have become `Suggested` dependencies: `httr`, `testthat`, and `rstudioapi`. This should allow snappier CI builds for usages not relying on some more "peripheral" features of the package.
* Error message for mismatched starts and ends of exclusion ranges is now more helpful. (#571, #860, @AshesITR and @danielinteractive)
* Debugging functions (`browser()`, `debug()`, `debugcall()`, `debugonce()`, `trace()`, `undebug()`, `untrace()`) are now part of the default set of undesirable functions to help prevent them from being committed by mistake. (#876, @michaelchirico)
* A new set of linters provided as part of Google's extension to the tidyverse style guide, available as `google_linters` (#884, @michaelchirico)
+ `expect_null_linter()` Require usage of `expect_null(x)` over `expect_equal(x, NULL)` and similar

# lintr 2.0.1

Expand Down
43 changes: 43 additions & 0 deletions R/expect_null_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#' @describeIn google-linters Require usage of expect_null(x) over expect_equal(x, NULL) and similar
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
#' [testthat::expect_null()] exists specifically for testing for `NULL` objects.
AshesITR marked this conversation as resolved.
Show resolved Hide resolved
#' [testthat::expect_equal()], [testthat::expect_identical()], and
#' [testthat::expect_true()] can also be used for such tests,
#' but it is better to use the tailored function instead.
#' @export
expect_null_linter <- function() {
AshesITR marked this conversation as resolved.
Show resolved Hide resolved
Linter(function(source_file) {
if (length(source_file$parsed_content) == 0L) {
return(list())
}

xml <- source_file$xml_parsed_content

# for expect_{equal,identical}(x, NULL)
equal_expr_cond <- sprintf(
"SYMBOL_FUNCTION_CALL[%s] and following-sibling::expr[2][NULL_CONST]",
AshesITR marked this conversation as resolved.
Show resolved Hide resolved
xp_text_in_table(c("expect_equal", "expect_identical"))
)

# for expect_true(is.null(x))
is_null_call <- "SYMBOL_FUNCTION_CALL[text() = 'is.null']"
true_expr_cond <- xp_and(
"SYMBOL_FUNCTION_CALL[text() = 'expect_true']",
sprintf("following-sibling::expr[1][expr[%s]]", is_null_call)
AshesITR marked this conversation as resolved.
Show resolved Hide resolved
)

xpath <- sprintf("//expr[(%s) or (%s)]", equal_expr_cond, true_expr_cond)

bad_expr <- xml2::xml_find_all(xml, xpath)

return(lapply(
bad_expr,
xml_nodes_to_lint,
source_file = source_file,
message = paste(
"expect_null(x) is better than expect_equal(x, NULL),",
"expect_identical(x, NULL), or expect_true(is.null(x))."
),
type = "warning"
))
})
}
9 changes: 9 additions & 0 deletions R/google_linters.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#' @title Linters for the Google extension to the tidyverse style guide
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
#'
#' Google uses a fork of the tidyverse style guide, pursuant to which
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @michaelquinn32 for copy here & in NEWS

#' its R users have written a bundle of linters, many of which
#' are generally useful for writing good R code. These linters
#' help encourage code consistency, readability, and best practices.
#'
#' @name google-linters
NULL
29 changes: 0 additions & 29 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -199,35 +199,6 @@ unescape <- function(str, q="`") {
str
}

# like `text() %in% table`, translated to XPath 1.0
xp_text_in_table <- function(table) paste("text() = ", quote_wrap(table, "'"), collapse = " or ")

# convert an XML match into a Lint
xml_nodes_to_lint <- function(xml, source_file, message,
type = c("style", "warning", "error"),
offset = 0L, global = FALSE) {
type <- match.arg(type, c("style", "warning", "error"))
line1 <- xml2::xml_attr(xml, "line1")[1]
col1 <- as.integer(xml2::xml_attr(xml, "col1")) + offset

line_elt <- if (global) "file_lines" else "lines"

if (xml2::xml_attr(xml, "line2") == line1) {
col2 <- as.integer(xml2::xml_attr(xml, "col2")) + offset
} else {
col2 <- unname(nchar(source_file[[line_elt]][line1]))
}
return(Lint(
filename = source_file$filename,
line_number = as.integer(line1),
column_number = as.integer(col1),
type = type,
message = message,
line = source_file[[line_elt]][line1],
ranges = list(c(col1 - offset, col2))
))
}

# interface to work like options() or setwd() -- returns the old value for convenience
set_lang <- function(new_lang) {
old_lang <- Sys.getenv("LANGUAGE", unset = NA)
Expand Down
53 changes: 53 additions & 0 deletions R/xp_utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# utils for working with xpaths

# like `text() %in% table`, translated to XPath 1.0
xp_text_in_table <- function(table) paste("text() = ", quote_wrap(table, "'"), collapse = " or ")

# convert an XML match into a Lint
xml_nodes_to_lint <- function(xml, source_file, message,
type = c("style", "warning", "error"),
offset = 0L,
global = FALSE) {
type <- match.arg(type, c("style", "warning", "error"))
line1 <- xml2::xml_attr(xml, "line1")[1]
col1 <- as.integer(xml2::xml_attr(xml, "col1")) + offset

line_elt <- if (global) "file_lines" else "lines"

if (xml2::xml_attr(xml, "line2") == line1) {
col2 <- as.integer(xml2::xml_attr(xml, "col2")) + offset
} else {
col2 <- unname(nchar(source_file[[line_elt]][line1]))
}
return(Lint(
filename = source_file$filename,
line_number = as.integer(line1),
column_number = as.integer(col1),
type = type,
message = message,
line = source_file[[line_elt]][line1],
ranges = list(c(col1 - offset, col2))
))
}

#' Safer wrapper for paste(..., sep = " and ")
#'
#' The intent is to use this for readability when combining XPath conditions so
#' the `...` elements should be in that language, but this is not enforced.
#'
#' Inputs are also wrapped in `()` so as not to risk mixing operator precedence.
#'
#' @param ... Series of conditions
#' @noRd
xp_and <- function(...) sprintf("(%s)", paste(..., sep = ") and ("))

#' Safer wrapper for paste(..., sep = " or ")
#'
#' The intent is to use this for readability when combining XPath conditions so
#' the `...` elements should be in that language, but this is not enforced.
#'
#' Inputs are also wrapped in `()` so as not to risk mixing operator precedence.
#'
#' @param ... Series of conditions
#' @noRd
xp_or <- function(...) sprintf("(%s)", paste(..., sep = ") or ("))
31 changes: 31 additions & 0 deletions man/google-linters.Rd

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

36 changes: 36 additions & 0 deletions tests/testthat/test-expect_null_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
test_that("expect_null_linter skips allowed usages", {
# NB: this usage should be expect_false(), but this linter should
# nevertheless apply (e.g. for maintainers not using ExpectNotLinter)
expect_lint("expect_true(!is.null(x))", NULL, expect_null_linter())
# NB: also applies to tinytest, but it's sufficient to test testthat
expect_lint(
"testthat::expect_true(!is.null(x))",
NULL,
expect_null_linter()
)

# length-0 could be NULL, but could be integer() or list(), so let it pass
expect_lint("expect_length(x, 0L)", NULL, expect_null_linter())
})

test_that("expect_null_linter blocks simple disallowed usages", {
expect_lint(
"expect_equal(x, NULL)",
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
"expect_null\\(x\\) is better than expect_equal\\(x, NULL\\)",
expect_null_linter()
)

# expect_identical is treated the same as expect_equal
expect_lint(
"testthat::expect_identical(x, NULL)",
"expect_null\\(x\\) is better than expect_equal\\(x, NULL\\)",
expect_null_linter()
)

# different equivalent usage
expect_lint(
"expect_true(is.null(foo(x)))",
"expect_null\\(x\\) is better than expect_equal\\(x, NULL\\)",
expect_null_linter()
)
})