From 3dae52e22b4539caebe91639c1479875b473e284 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 14 Aug 2023 16:30:43 +0000 Subject: [PATCH 1/9] New scalar_in_linter() --- DESCRIPTION | 1 + NAMESPACE | 1 + R/scalar_in_linter.R | 44 ++++++++++++++++++++++++++ inst/lintr/linters.csv | 1 + man/best_practices_linters.Rd | 1 + man/consistency_linters.Rd | 1 + man/efficiency_linters.Rd | 1 + man/linters.Rd | 9 +++--- man/readability_linters.Rd | 1 + man/scalar_in_linter.Rd | 23 ++++++++++++++ tests/testthat/test-scalar_in_linter.R | 29 +++++++++++++++++ 11 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 R/scalar_in_linter.R create mode 100644 man/scalar_in_linter.Rd create mode 100644 tests/testthat/test-scalar_in_linter.R diff --git a/DESCRIPTION b/DESCRIPTION index cde82503f..fef1979a9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -149,6 +149,7 @@ Collate: 'redundant_ifelse_linter.R' 'regex_subset_linter.R' 'routine_registration_linter.R' + 'scalar_in_linter.R' 'semicolon_linter.R' 'seq_linter.R' 'settings.R' diff --git a/NAMESPACE b/NAMESPACE index e70060a02..b1a33f293 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -114,6 +114,7 @@ export(redundant_ifelse_linter) export(regex_subset_linter) export(routine_registration_linter) export(sarif_output) +export(scalar_in_linter) export(semicolon_linter) export(semicolon_terminator_linter) export(seq_linter) diff --git a/R/scalar_in_linter.R b/R/scalar_in_linter.R new file mode 100644 index 000000000..771401e6d --- /dev/null +++ b/R/scalar_in_linter.R @@ -0,0 +1,44 @@ +#' Block usage like x %in% "a" +#' +#' `vector %in% set` is appropriate for matching a vector to a set, but if +#' that set has size 1, `==` is more appropriate. `%chin%` from `data.table` +#' is matched as well. +#' +#' `scalar %in% vector` is OK, because the alternative (`any(vector == scalar)`) +#' is more circuitous & potentially less clear. +#' +#' @evalRd rd_tags("scalar_in_linter") +#' @seealso [linters] for a complete list of linters available in lintr. +#' @export +scalar_in_linter <- function() { + # TODO(michaelchirico): this could be extended to work for a few more cases, e.g. + # x %in% c(1) _and_ x %in% 1+3i. Deprioritized because the former would be + # caught by the concatentation linter, and I assume the latter is quite rare + # all of logical, integer, double, hex, complex are parsed as NUM_CONST + xpath <- " + //SPECIAL[text() = '%in%' or text() = '%chin%'] + /following-sibling::expr[NUM_CONST or STR_CONST] + /parent::expr + " + + return(Linter(function(source_expression) { + if (!is_lint_level(source_expression, "expression")) { + return(list()) + } + + xml <- source_expression$xml_parsed_content + + bad_expr <- xml_find_all(xml, xpath) + in_op <- xml_find_chr(bad_expr, "string(SPECIAL)") + + xml_nodes_to_lints( + bad_expr, + source_expression = source_expression, + lint_message = paste0( + "Use == to match length-1 scalars, not ", in_op, ". ", + "Note that == preserves NA where ", in_op, " does not." + ), + type = "warning" + ) + })) +} diff --git a/inst/lintr/linters.csv b/inst/lintr/linters.csv index 2d4ef2504..82699628b 100644 --- a/inst/lintr/linters.csv +++ b/inst/lintr/linters.csv @@ -71,6 +71,7 @@ redundant_equals_linter,best_practices readability efficiency common_mistakes redundant_ifelse_linter,best_practices efficiency consistency configurable regex_subset_linter,best_practices efficiency routine_registration_linter,best_practices efficiency robustness +scalar_in_linter,readability consistency best_practices efficiency semicolon_linter,style readability default configurable semicolon_terminator_linter,style readability deprecated configurable seq_linter,robustness efficiency consistency best_practices default diff --git a/man/best_practices_linters.Rd b/man/best_practices_linters.Rd index 99b27e23f..6016a5f66 100644 --- a/man/best_practices_linters.Rd +++ b/man/best_practices_linters.Rd @@ -51,6 +51,7 @@ The following linters are tagged with 'best_practices': \item{\code{\link{redundant_ifelse_linter}}} \item{\code{\link{regex_subset_linter}}} \item{\code{\link{routine_registration_linter}}} +\item{\code{\link{scalar_in_linter}}} \item{\code{\link{seq_linter}}} \item{\code{\link{sort_linter}}} \item{\code{\link{system_file_linter}}} diff --git a/man/consistency_linters.Rd b/man/consistency_linters.Rd index dba269fc9..ba9ec253e 100644 --- a/man/consistency_linters.Rd +++ b/man/consistency_linters.Rd @@ -29,6 +29,7 @@ The following linters are tagged with 'consistency': \item{\code{\link{paste_linter}}} \item{\code{\link{quotes_linter}}} \item{\code{\link{redundant_ifelse_linter}}} +\item{\code{\link{scalar_in_linter}}} \item{\code{\link{seq_linter}}} \item{\code{\link{system_file_linter}}} \item{\code{\link{T_and_F_symbol_linter}}} diff --git a/man/efficiency_linters.Rd b/man/efficiency_linters.Rd index f2f27d3dc..cc5931d4b 100644 --- a/man/efficiency_linters.Rd +++ b/man/efficiency_linters.Rd @@ -27,6 +27,7 @@ The following linters are tagged with 'efficiency': \item{\code{\link{redundant_ifelse_linter}}} \item{\code{\link{regex_subset_linter}}} \item{\code{\link{routine_registration_linter}}} +\item{\code{\link{scalar_in_linter}}} \item{\code{\link{seq_linter}}} \item{\code{\link{sort_linter}}} \item{\code{\link{string_boundary_linter}}} diff --git a/man/linters.Rd b/man/linters.Rd index 921c48d61..c58041e2c 100644 --- a/man/linters.Rd +++ b/man/linters.Rd @@ -17,17 +17,17 @@ see also \code{\link[=available_tags]{available_tags()}}. \section{Tags}{ The following tags exist: \itemize{ -\item{\link[=best_practices_linters]{best_practices} (52 linters)} +\item{\link[=best_practices_linters]{best_practices} (53 linters)} \item{\link[=common_mistakes_linters]{common_mistakes} (7 linters)} \item{\link[=configurable_linters]{configurable} (29 linters)} -\item{\link[=consistency_linters]{consistency} (20 linters)} +\item{\link[=consistency_linters]{consistency} (21 linters)} \item{\link[=correctness_linters]{correctness} (7 linters)} \item{\link[=default_linters]{default} (25 linters)} \item{\link[=deprecated_linters]{deprecated} (8 linters)} -\item{\link[=efficiency_linters]{efficiency} (23 linters)} +\item{\link[=efficiency_linters]{efficiency} (24 linters)} \item{\link[=executing_linters]{executing} (5 linters)} \item{\link[=package_development_linters]{package_development} (14 linters)} -\item{\link[=readability_linters]{readability} (50 linters)} +\item{\link[=readability_linters]{readability} (51 linters)} \item{\link[=robustness_linters]{robustness} (14 linters)} \item{\link[=style_linters]{style} (36 linters)} } @@ -102,6 +102,7 @@ The following linters exist: \item{\code{\link{redundant_ifelse_linter}} (tags: best_practices, configurable, consistency, efficiency)} \item{\code{\link{regex_subset_linter}} (tags: best_practices, efficiency)} \item{\code{\link{routine_registration_linter}} (tags: best_practices, efficiency, robustness)} +\item{\code{\link{scalar_in_linter}} (tags: best_practices, consistency, efficiency, readability)} \item{\code{\link{semicolon_linter}} (tags: configurable, default, readability, style)} \item{\code{\link{seq_linter}} (tags: best_practices, consistency, default, efficiency, robustness)} \item{\code{\link{sort_linter}} (tags: best_practices, efficiency, readability)} diff --git a/man/readability_linters.Rd b/man/readability_linters.Rd index ece2b5912..f3d34f130 100644 --- a/man/readability_linters.Rd +++ b/man/readability_linters.Rd @@ -49,6 +49,7 @@ The following linters are tagged with 'readability': \item{\code{\link{pipe_continuation_linter}}} \item{\code{\link{quotes_linter}}} \item{\code{\link{redundant_equals_linter}}} +\item{\code{\link{scalar_in_linter}}} \item{\code{\link{semicolon_linter}}} \item{\code{\link{sort_linter}}} \item{\code{\link{spaces_inside_linter}}} diff --git a/man/scalar_in_linter.Rd b/man/scalar_in_linter.Rd new file mode 100644 index 000000000..3108ca8c5 --- /dev/null +++ b/man/scalar_in_linter.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/scalar_in_linter.R +\name{scalar_in_linter} +\alias{scalar_in_linter} +\title{Block usage like x \%in\% "a"} +\usage{ +scalar_in_linter() +} +\description{ +\code{vector \%in\% set} is appropriate for matching a vector to a set, but if +that set has size 1, \code{==} is more appropriate. \verb{\%chin\%} from \code{data.table} +is matched as well. +} +\details{ +\code{scalar \%in\% vector} is OK, because the alternative (\code{any(vector == scalar)}) +is more circuitous & potentially less clear. +} +\seealso{ +\link{linters} for a complete list of linters available in lintr. +} +\section{Tags}{ +\link[=best_practices_linters]{best_practices}, \link[=consistency_linters]{consistency}, \link[=efficiency_linters]{efficiency}, \link[=readability_linters]{readability} +} diff --git a/tests/testthat/test-scalar_in_linter.R b/tests/testthat/test-scalar_in_linter.R new file mode 100644 index 000000000..39327845b --- /dev/null +++ b/tests/testthat/test-scalar_in_linter.R @@ -0,0 +1,29 @@ +test_that("scalar_in_linter skips allowed usages", { + linter <- scalar_in_linter() + + expect_lint("x %in% y", NULL, linter) + expect_lint("y %in% c('a', 'b')", NULL, linter) + expect_lint("c('a', 'b') %chin% x", NULL, linter) + expect_lint("z %in% 1:3", NULL, linter) + # scalars on LHS are fine (often used as `"col" %in% names(DF)`) + expect_lint("3L %in% x", NULL, linter) +}) + +test_that("scalar_in_linter blocks simple disallowed usages", { + linter <- scalar_in_linter() + lint_msg <- rex::rex("Use == to match length-1 scalars, not %in%.") + + expect_lint("x %in% 1", lint_msg, linter) + expect_lint("x %chin% 'a'", lint_msg, linter) +}) + +test_that("multiple lints are generated correctly", { + expect_lint( + trim_some('{ + x %in% 1 + y %chin% "a" + }'), + list("%in%", "%chin%"), + scalar_in_linter() + ) +}) From ca84e2bcbab8c10f19c824cd6dac19d79601f4a3 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 14 Aug 2023 16:33:13 +0000 Subject: [PATCH 2/9] NEWS --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index c9cec72c4..db2525d8d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -32,6 +32,7 @@ * `library_call_linter()` can detect if all library calls are not at the top of your script (#2027, @nicholas-masel). * `keyword_quote_linter()` for finding unnecessary or discouraged quoting of symbols in assignment, function arguments, or extraction (part of #884, @MichaelChirico). Quoting is unnecessary when the target is a valid R name, e.g. `c("a" = 1)` can be `c(a = 1)`. The same goes to assignment (`"a" <- 1`) and extraction (`x$"a"`). Where quoting is necessary, the linter encourages doing so with backticks (e.g. `` x$`a b` `` instead of `x$"a b"`). * `length_levels_linter()` for using the specific function `nlevels()` instead of checking `length(levels(x))` (part of #884, @MichaelChirico). +* `scalar_in_linter()` for discouraging `%in%` when the right-hand side is a scalar, e.g. `x %in% 1` (part of #884, @MichaelChirico). ## Changes to defaults From e3cb1e3c71c48c6e1fb96b4e3ffb3d6251603cb5 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 14 Aug 2023 16:37:42 +0000 Subject: [PATCH 3/9] specific TODO --- R/scalar_in_linter.R | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/R/scalar_in_linter.R b/R/scalar_in_linter.R index 771401e6d..58ae8f7c2 100644 --- a/R/scalar_in_linter.R +++ b/R/scalar_in_linter.R @@ -11,10 +11,8 @@ #' @seealso [linters] for a complete list of linters available in lintr. #' @export scalar_in_linter <- function() { - # TODO(michaelchirico): this could be extended to work for a few more cases, e.g. - # x %in% c(1) _and_ x %in% 1+3i. Deprioritized because the former would be - # caught by the concatentation linter, and I assume the latter is quite rare - # all of logical, integer, double, hex, complex are parsed as NUM_CONST + # TODO(#2085): Extend to include other cases where the RHS is clearly a scalar + # NB: all of logical, integer, double, hex, complex are parsed as NUM_CONST xpath <- " //SPECIAL[text() = '%in%' or text() = '%chin%'] /following-sibling::expr[NUM_CONST or STR_CONST] From bf6053f59a8bcafcaa4a6712dd230b6825717fac Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Mon, 14 Aug 2023 09:48:08 -0700 Subject: [PATCH 4/9] Update test-scalar_in_linter.R --- tests/testthat/test-scalar_in_linter.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-scalar_in_linter.R b/tests/testthat/test-scalar_in_linter.R index 39327845b..92c0c8d87 100644 --- a/tests/testthat/test-scalar_in_linter.R +++ b/tests/testthat/test-scalar_in_linter.R @@ -11,10 +11,11 @@ test_that("scalar_in_linter skips allowed usages", { test_that("scalar_in_linter blocks simple disallowed usages", { linter <- scalar_in_linter() - lint_msg <- rex::rex("Use == to match length-1 scalars, not %in%.") + lint_in_msg <- rex::rex("Use == to match length-1 scalars, not %in%.") + lint_chin_msg <- rex::rex("Use == to match length-1 scalars, not %chin%.") - expect_lint("x %in% 1", lint_msg, linter) - expect_lint("x %chin% 'a'", lint_msg, linter) + expect_lint("x %in% 1", lint_in_msg, linter) + expect_lint("x %chin% 'a'", lint_chin_msg, linter) }) test_that("multiple lints are generated correctly", { From a9063989b4640ae0d440bd6b5d65c22cfb9c217b Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Wed, 16 Aug 2023 06:24:44 +0000 Subject: [PATCH 5/9] progress customizing for NA --- R/scalar_in_linter.R | 12 ++++++++---- tests/testthat/test-scalar_in_linter.R | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/R/scalar_in_linter.R b/R/scalar_in_linter.R index 58ae8f7c2..8256eff4b 100644 --- a/R/scalar_in_linter.R +++ b/R/scalar_in_linter.R @@ -28,14 +28,18 @@ scalar_in_linter <- function() { bad_expr <- xml_find_all(xml, xpath) in_op <- xml_find_chr(bad_expr, "string(SPECIAL)") + # NB: NA_character_ is also a NUM_CONST, somewhat strangely. + rhs_na <- startsWith(xml_find_chr(bad_expr, "string(expr[2]/NUM_CONST)"), "NA") + lint_msg <- ifelse( + rhs_na, + paste0("Use is.na() to check missingness, not ", in_op, "."), + paste0("Use == to match length-1 scalars, not ", in_op, ". Note that == preserves NA where ", in_op, " does not.") + ) xml_nodes_to_lints( bad_expr, source_expression = source_expression, - lint_message = paste0( - "Use == to match length-1 scalars, not ", in_op, ". ", - "Note that == preserves NA where ", in_op, " does not." - ), + lint_message = lint_msg, type = "warning" ) })) diff --git a/tests/testthat/test-scalar_in_linter.R b/tests/testthat/test-scalar_in_linter.R index 39327845b..b7645ce04 100644 --- a/tests/testthat/test-scalar_in_linter.R +++ b/tests/testthat/test-scalar_in_linter.R @@ -27,3 +27,20 @@ test_that("multiple lints are generated correctly", { scalar_in_linter() ) }) + +test_that("%in% NA recommends using is.na() alone, not ==", { + linter <- scalar_in_linter() + + expect_lint("x %in% NA", NULL, linter) + expect_lint("x %in% NA_real_", NULL, linter) + expect_lint( + trim_some("{ + x %in% NA + x %chin% 2 + x %chin% NA_character_ + x %in% 'd' + }"), + NULL, + linter + ) +}) From 30f467ec1e312f238b34a1d0a2606d7ae1ffdf6b Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Wed, 16 Aug 2023 06:28:14 +0000 Subject: [PATCH 6/9] correct tests --- tests/testthat/test-scalar_in_linter.R | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/testthat/test-scalar_in_linter.R b/tests/testthat/test-scalar_in_linter.R index 9c1fa0a63..8408e87fa 100644 --- a/tests/testthat/test-scalar_in_linter.R +++ b/tests/testthat/test-scalar_in_linter.R @@ -18,22 +18,26 @@ test_that("scalar_in_linter blocks simple disallowed usages", { expect_lint("x %chin% 'a'", lint_chin_msg, linter) }) +test_that("%in% NA recommends using is.na() alone, not ==", { + linter <- scalar_in_linter() + lint_msg <- rex::rex("Use is.na() to check missingness, not %in%.") + + expect_lint("x %in% NA", lint_msg, linter) + expect_lint("x %in% NA_real_", lint_msg, linter) +}) + test_that("multiple lints are generated correctly", { + linter <- scalar_in_linter() + expect_lint( trim_some('{ x %in% 1 y %chin% "a" }'), list("%in%", "%chin%"), - scalar_in_linter() + linter ) -}) - -test_that("%in% NA recommends using is.na() alone, not ==", { - linter <- scalar_in_linter() - expect_lint("x %in% NA", NULL, linter) - expect_lint("x %in% NA_real_", NULL, linter) expect_lint( trim_some("{ x %in% NA @@ -41,7 +45,12 @@ test_that("%in% NA recommends using is.na() alone, not ==", { x %chin% NA_character_ x %in% 'd' }"), - NULL, + list( + rex::rex("Use is.na() to check missingness, not %in%."), + rex::rex("Use == to match length-1 scalars, not %chin%."), + rex::rex("Use is.na() to check missingness, not %chin%."), + rex::rex("Use == to match length-1 scalars, not %in%.") + ), linter ) }) From 35f3ed880ee2073c8d38de3cf6179c3f917d365a Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Wed, 16 Aug 2023 21:36:07 +0000 Subject: [PATCH 7/9] revert most changes related to NA (just skip linting) --- R/scalar_in_linter.R | 9 ++------- tests/testthat/test-scalar_in_linter.R | 24 ------------------------ 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/R/scalar_in_linter.R b/R/scalar_in_linter.R index 8256eff4b..ea64db002 100644 --- a/R/scalar_in_linter.R +++ b/R/scalar_in_linter.R @@ -15,7 +15,7 @@ scalar_in_linter <- function() { # NB: all of logical, integer, double, hex, complex are parsed as NUM_CONST xpath <- " //SPECIAL[text() = '%in%' or text() = '%chin%'] - /following-sibling::expr[NUM_CONST or STR_CONST] + /following-sibling::expr[NUM_CONST[not(starts-with(text(), 'NA'))] or STR_CONST] /parent::expr " @@ -28,13 +28,8 @@ scalar_in_linter <- function() { bad_expr <- xml_find_all(xml, xpath) in_op <- xml_find_chr(bad_expr, "string(SPECIAL)") - # NB: NA_character_ is also a NUM_CONST, somewhat strangely. - rhs_na <- startsWith(xml_find_chr(bad_expr, "string(expr[2]/NUM_CONST)"), "NA") - lint_msg <- ifelse( - rhs_na, - paste0("Use is.na() to check missingness, not ", in_op, "."), + lint_msg <- paste0("Use == to match length-1 scalars, not ", in_op, ". Note that == preserves NA where ", in_op, " does not.") - ) xml_nodes_to_lints( bad_expr, diff --git a/tests/testthat/test-scalar_in_linter.R b/tests/testthat/test-scalar_in_linter.R index 8408e87fa..190861c98 100644 --- a/tests/testthat/test-scalar_in_linter.R +++ b/tests/testthat/test-scalar_in_linter.R @@ -18,14 +18,6 @@ test_that("scalar_in_linter blocks simple disallowed usages", { expect_lint("x %chin% 'a'", lint_chin_msg, linter) }) -test_that("%in% NA recommends using is.na() alone, not ==", { - linter <- scalar_in_linter() - lint_msg <- rex::rex("Use is.na() to check missingness, not %in%.") - - expect_lint("x %in% NA", lint_msg, linter) - expect_lint("x %in% NA_real_", lint_msg, linter) -}) - test_that("multiple lints are generated correctly", { linter <- scalar_in_linter() @@ -37,20 +29,4 @@ test_that("multiple lints are generated correctly", { list("%in%", "%chin%"), linter ) - - expect_lint( - trim_some("{ - x %in% NA - x %chin% 2 - x %chin% NA_character_ - x %in% 'd' - }"), - list( - rex::rex("Use is.na() to check missingness, not %in%."), - rex::rex("Use == to match length-1 scalars, not %chin%."), - rex::rex("Use is.na() to check missingness, not %chin%."), - rex::rex("Use == to match length-1 scalars, not %in%.") - ), - linter - ) }) From 89f1fde14fe0b5825d62059d4a993df7950a7db1 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Wed, 16 Aug 2023 22:17:26 +0000 Subject: [PATCH 8/9] tests for NA --- tests/testthat/test-scalar_in_linter.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/testthat/test-scalar_in_linter.R b/tests/testthat/test-scalar_in_linter.R index 190861c98..d182e57a3 100644 --- a/tests/testthat/test-scalar_in_linter.R +++ b/tests/testthat/test-scalar_in_linter.R @@ -7,6 +7,11 @@ test_that("scalar_in_linter skips allowed usages", { expect_lint("z %in% 1:3", NULL, linter) # scalars on LHS are fine (often used as `"col" %in% names(DF)`) expect_lint("3L %in% x", NULL, linter) + + # this should be NA, but it more directly uses the "always TRUE/FALSE, _not_ NA" + # aspect of %in%, so we delegate linting here to equals_na_linter() + expect_lint("x %in% NA", NULL, linter) + expect_lint("x %in% NA_character_", NULL, linter) }) test_that("scalar_in_linter blocks simple disallowed usages", { From 7481e5df6dc19461d88f9178c035868e79d04c00 Mon Sep 17 00:00:00 2001 From: Michael Chirico Date: Wed, 6 Sep 2023 16:54:41 +0000 Subject: [PATCH 9/9] tweak comment --- tests/testthat/test-scalar_in_linter.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-scalar_in_linter.R b/tests/testthat/test-scalar_in_linter.R index d182e57a3..215639251 100644 --- a/tests/testthat/test-scalar_in_linter.R +++ b/tests/testthat/test-scalar_in_linter.R @@ -8,7 +8,7 @@ test_that("scalar_in_linter skips allowed usages", { # scalars on LHS are fine (often used as `"col" %in% names(DF)`) expect_lint("3L %in% x", NULL, linter) - # this should be NA, but it more directly uses the "always TRUE/FALSE, _not_ NA" + # this should be is.na(x), but it more directly uses the "always TRUE/FALSE, _not_ NA" # aspect of %in%, so we delegate linting here to equals_na_linter() expect_lint("x %in% NA", NULL, linter) expect_lint("x %in% NA_character_", NULL, linter)