From 36442637fee44c7292c3b4fdc98a7dba49872fe8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 18:20:03 +0000 Subject: [PATCH 1/2] Add ggplot_aes_linter: no aes() nested inside ggplot() Part of the no-nested-calls policy (nested_calls = TRUE). nested_call_linter allows aes() as an argument, so ggplot(df, aes(x, y)) passed lint. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_019xfc9nm2UYhuLY1k6irzLP --- lms/NAMESPACE | 1 + lms/R/default_linters.R | 8 ++- lms/R/ggplot_aes_linter.R | 42 ++++++++++++++ lms/README.md | 4 +- lms/man/default_linters.Rd | 5 +- lms/man/ggplot_aes_linter.Rd | 24 ++++++++ lms/tests/testthat/test-ggplot_aes_linter.R | 61 +++++++++++++++++++++ 7 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 lms/R/ggplot_aes_linter.R create mode 100644 lms/man/ggplot_aes_linter.Rd create mode 100644 lms/tests/testthat/test-ggplot_aes_linter.R diff --git a/lms/NAMESPACE b/lms/NAMESPACE index 3e321d62..f68548f5 100644 --- a/lms/NAMESPACE +++ b/lms/NAMESPACE @@ -4,5 +4,6 @@ export(bare_condition_linter) export(default_linters) export(function_length_linter) export(function_location_linter) +export(ggplot_aes_linter) export(nested_call_linter) export(undesirable_functions) diff --git a/lms/R/default_linters.R b/lms/R/default_linters.R index e2e26e5c..e519e142 100644 --- a/lms/R/default_linters.R +++ b/lms/R/default_linters.R @@ -9,8 +9,9 @@ #' exclusions <- list(...) # repo-specific, stays local #' ``` #' -#' @param nested_calls Whether to include [nested_call_linter()] and -#' [bare_condition_linter()], the no-nested-calls policy. Off by default +#' @param nested_calls Whether to include [nested_call_linter()], +#' [bare_condition_linter()] and [ggplot_aes_linter()], +#' the no-nested-calls policy. Off by default #' while existing code is swept; turn them on in a repo's `.lintr.R` with #' `lms::default_linters(nested_calls = TRUE)`. #' @return A list of linters, as produced by @@ -39,7 +40,8 @@ default_linters <- function(nested_calls = FALSE) { linters, list( nested_call_linter = nested_call_linter(), - bare_condition_linter = bare_condition_linter() + bare_condition_linter = bare_condition_linter(), + ggplot_aes_linter = ggplot_aes_linter() ) ) } diff --git a/lms/R/ggplot_aes_linter.R b/lms/R/ggplot_aes_linter.R new file mode 100644 index 00000000..993eb017 --- /dev/null +++ b/lms/R/ggplot_aes_linter.R @@ -0,0 +1,42 @@ +#' ggplot() aesthetic-mapping linter +#' +#' Flags an `aes()` call written inside a call to `ggplot()`, as in +#' `ggplot(df, aes(x, y))`. Pipe the data into a bare `ggplot()` layer, +#' then add the mapping as its own layer: `df |> ggplot() + aes(x, y)`. +#' This is the no-nested-calls policy applied to `ggplot()`, +#' which [nested_call_linter()] does not enforce +#' because it allows `aes()` as an argument. +#' +#' Both `ggplot()` and `ggplot2::ggplot()` are checked, +#' and the mapping may be named (`mapping = aes(x, y)`) or positional. +#' An `aes()` inside a layer such as `geom_point(aes(color = g))` is not flagged. +#' +#' @return A [lintr::Linter()] object. +#' @export +ggplot_aes_linter <- function() { + xpath <- paste0( + "//expr[expr[1]/SYMBOL_FUNCTION_CALL[text() = 'ggplot']]", + "/expr[position() > 1][expr[1]/SYMBOL_FUNCTION_CALL[text() = 'aes']]" + ) + + lintr::Linter(function(source_expression) { + if (!lintr::is_lint_level(source_expression, "expression")) { + return(list()) + } + + nested <- xml2::xml_find_all( + source_expression$xml_parsed_content, + xpath + ) + + lintr::xml_nodes_to_lints( + nested, + source_expression = source_expression, + lint_message = paste( + "Don't nest aes() inside ggplot(): pipe the data into ggplot()", + "and add the mapping as its own layer (`+ aes(x, y)`)." + ), + type = "warning" + ) + }) +} diff --git a/lms/README.md b/lms/README.md index a767ada6..d21cc929 100644 --- a/lms/README.md +++ b/lms/README.md @@ -32,7 +32,9 @@ It exports: and for tidyselect helpers, `aes()` and `vars()`, it flags nothing. The second flags an `if()` or `while()` condition that is not already a bare name or `TRUE`/`FALSE`, as in `while (TRUE)`. - Both are off by default while existing code is swept; + `ggplot_aes_linter()` is the third: it flags `aes()` written inside `ggplot()`, + so pipe the data into a bare `ggplot()` and add the mapping as its own layer (`df |> ggplot() + aes(x, y)`). + All three are off by default while existing code is swept; turn them on with `lms::default_linters(nested_calls = TRUE)`. - `undesirable_functions()` --- the underlying undesirable-function map, if a repo needs to extend it. diff --git a/lms/man/default_linters.Rd b/lms/man/default_linters.Rd index 4ed16b30..21446079 100644 --- a/lms/man/default_linters.Rd +++ b/lms/man/default_linters.Rd @@ -7,8 +7,9 @@ default_linters(nested_calls = FALSE) } \arguments{ -\item{nested_calls}{Whether to include \code{\link[=nested_call_linter]{nested_call_linter()}} and -\code{\link[=bare_condition_linter]{bare_condition_linter()}}, the no-nested-calls policy. Off by default +\item{nested_calls}{Whether to include \code{\link[=nested_call_linter]{nested_call_linter()}}, +\code{\link[=bare_condition_linter]{bare_condition_linter()}} and \code{\link[=ggplot_aes_linter]{ggplot_aes_linter()}}, +the no-nested-calls policy. Off by default while existing code is swept; turn them on in a repo's \code{.lintr.R} with \code{lms::default_linters(nested_calls = TRUE)}.} } diff --git a/lms/man/ggplot_aes_linter.Rd b/lms/man/ggplot_aes_linter.Rd new file mode 100644 index 00000000..9542be8d --- /dev/null +++ b/lms/man/ggplot_aes_linter.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ggplot_aes_linter.R +\name{ggplot_aes_linter} +\alias{ggplot_aes_linter} +\title{ggplot() aesthetic-mapping linter} +\usage{ +ggplot_aes_linter() +} +\value{ +A \code{\link[lintr:Linter]{lintr::Linter()}} object. +} +\description{ +Flags an \code{aes()} call written inside a call to \code{ggplot()}, as in +\code{ggplot(df, aes(x, y))}. Pipe the data into a bare \code{ggplot()} layer, +then add the mapping as its own layer: \code{df |> ggplot() + aes(x, y)}. +This is the no-nested-calls policy applied to \code{ggplot()}, +which \code{\link[=nested_call_linter]{nested_call_linter()}} does not enforce +because it allows \code{aes()} as an argument. +} +\details{ +Both \code{ggplot()} and \code{ggplot2::ggplot()} are checked, +and the mapping may be named (\code{mapping = aes(x, y)}) or positional. +An \code{aes()} inside a layer such as \code{geom_point(aes(color = g))} is not flagged. +} diff --git a/lms/tests/testthat/test-ggplot_aes_linter.R b/lms/tests/testthat/test-ggplot_aes_linter.R new file mode 100644 index 00000000..65971018 --- /dev/null +++ b/lms/tests/testthat/test-ggplot_aes_linter.R @@ -0,0 +1,61 @@ +message <- "Don't nest aes\\(\\) inside ggplot\\(\\)" + +test_that("aes() inside ggplot() is flagged", { + lintr::expect_lint( + "ggplot(df, aes(x, y))", + message, + ggplot_aes_linter() + ) + lintr::expect_lint( + "ggplot(aes(x, y))", + message, + ggplot_aes_linter() + ) +}) + +test_that("the namespaced and named forms are flagged", { + lintr::expect_lint( + "ggplot2::ggplot(df, ggplot2::aes(x, y))", + message, + ggplot_aes_linter() + ) + lintr::expect_lint( + "ggplot(df, mapping = aes(x, y))", + message, + ggplot_aes_linter() + ) + lintr::expect_lint( + "df |> ggplot2::ggplot(ggplot2::aes(x, y))", + message, + ggplot_aes_linter() + ) +}) + +test_that("aes() added as its own layer is not flagged", { + lintr::expect_no_lint( + "df |> ggplot2::ggplot() + ggplot2::aes(x, y)", + ggplot_aes_linter() + ) + lintr::expect_no_lint( + "fig_aes <- aes(x, y)\nggplot(df, fig_aes)", + ggplot_aes_linter() + ) + lintr::expect_no_lint("ggplot(df)", ggplot_aes_linter()) +}) + +test_that("aes() inside a layer is not flagged", { + lintr::expect_no_lint( + "ggplot(df, fig_aes) + geom_point(aes(colour = g))", + ggplot_aes_linter() + ) + lintr::expect_no_lint( + "ggplot2::geom_point(ggplot2::aes(colour = g))", + ggplot_aes_linter() + ) +}) + +test_that("default_linters(nested_calls = TRUE) includes it", { + linters <- default_linters(nested_calls = TRUE) + expect_true("ggplot_aes_linter" %in% names(linters)) + expect_false("ggplot_aes_linter" %in% names(default_linters())) +}) From f5471d9287022594601e525bc199f9dd9a1b9ebb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 18:22:57 +0000 Subject: [PATCH 2/2] Rename to ggplot_args_linter and flag data passed to ggplot() Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_019xfc9nm2UYhuLY1k6irzLP --- lms/NAMESPACE | 2 +- lms/R/default_linters.R | 4 +- lms/R/ggplot_aes_linter.R | 42 ---------- lms/R/ggplot_args_linter.R | 58 ++++++++++++++ lms/README.md | 2 +- lms/man/default_linters.Rd | 2 +- lms/man/ggplot_aes_linter.Rd | 24 ------ lms/man/ggplot_args_linter.Rd | 28 +++++++ lms/tests/testthat/test-ggplot_aes_linter.R | 61 --------------- lms/tests/testthat/test-ggplot_args_linter.R | 80 ++++++++++++++++++++ 10 files changed, 171 insertions(+), 132 deletions(-) delete mode 100644 lms/R/ggplot_aes_linter.R create mode 100644 lms/R/ggplot_args_linter.R delete mode 100644 lms/man/ggplot_aes_linter.Rd create mode 100644 lms/man/ggplot_args_linter.Rd delete mode 100644 lms/tests/testthat/test-ggplot_aes_linter.R create mode 100644 lms/tests/testthat/test-ggplot_args_linter.R diff --git a/lms/NAMESPACE b/lms/NAMESPACE index f68548f5..95b51ece 100644 --- a/lms/NAMESPACE +++ b/lms/NAMESPACE @@ -4,6 +4,6 @@ export(bare_condition_linter) export(default_linters) export(function_length_linter) export(function_location_linter) -export(ggplot_aes_linter) +export(ggplot_args_linter) export(nested_call_linter) export(undesirable_functions) diff --git a/lms/R/default_linters.R b/lms/R/default_linters.R index e519e142..bfb3ce5b 100644 --- a/lms/R/default_linters.R +++ b/lms/R/default_linters.R @@ -10,7 +10,7 @@ #' ``` #' #' @param nested_calls Whether to include [nested_call_linter()], -#' [bare_condition_linter()] and [ggplot_aes_linter()], +#' [bare_condition_linter()] and [ggplot_args_linter()], #' the no-nested-calls policy. Off by default #' while existing code is swept; turn them on in a repo's `.lintr.R` with #' `lms::default_linters(nested_calls = TRUE)`. @@ -41,7 +41,7 @@ default_linters <- function(nested_calls = FALSE) { list( nested_call_linter = nested_call_linter(), bare_condition_linter = bare_condition_linter(), - ggplot_aes_linter = ggplot_aes_linter() + ggplot_args_linter = ggplot_args_linter() ) ) } diff --git a/lms/R/ggplot_aes_linter.R b/lms/R/ggplot_aes_linter.R deleted file mode 100644 index 993eb017..00000000 --- a/lms/R/ggplot_aes_linter.R +++ /dev/null @@ -1,42 +0,0 @@ -#' ggplot() aesthetic-mapping linter -#' -#' Flags an `aes()` call written inside a call to `ggplot()`, as in -#' `ggplot(df, aes(x, y))`. Pipe the data into a bare `ggplot()` layer, -#' then add the mapping as its own layer: `df |> ggplot() + aes(x, y)`. -#' This is the no-nested-calls policy applied to `ggplot()`, -#' which [nested_call_linter()] does not enforce -#' because it allows `aes()` as an argument. -#' -#' Both `ggplot()` and `ggplot2::ggplot()` are checked, -#' and the mapping may be named (`mapping = aes(x, y)`) or positional. -#' An `aes()` inside a layer such as `geom_point(aes(color = g))` is not flagged. -#' -#' @return A [lintr::Linter()] object. -#' @export -ggplot_aes_linter <- function() { - xpath <- paste0( - "//expr[expr[1]/SYMBOL_FUNCTION_CALL[text() = 'ggplot']]", - "/expr[position() > 1][expr[1]/SYMBOL_FUNCTION_CALL[text() = 'aes']]" - ) - - lintr::Linter(function(source_expression) { - if (!lintr::is_lint_level(source_expression, "expression")) { - return(list()) - } - - nested <- xml2::xml_find_all( - source_expression$xml_parsed_content, - xpath - ) - - lintr::xml_nodes_to_lints( - nested, - source_expression = source_expression, - lint_message = paste( - "Don't nest aes() inside ggplot(): pipe the data into ggplot()", - "and add the mapping as its own layer (`+ aes(x, y)`)." - ), - type = "warning" - ) - }) -} diff --git a/lms/R/ggplot_args_linter.R b/lms/R/ggplot_args_linter.R new file mode 100644 index 00000000..b98c57da --- /dev/null +++ b/lms/R/ggplot_args_linter.R @@ -0,0 +1,58 @@ +#' ggplot() arguments linter +#' +#' Flags any argument passed to `ggplot()`. +#' Pipe the primary data into a bare `ggplot()` layer, +#' then add the aesthetic mapping as its own layer: +#' `df |> ggplot() + aes(x, y)`, not `ggplot(df, aes(x, y))` +#' and not `ggplot(data = df)`. +#' This is the no-nested-calls policy applied to `ggplot()`, +#' which [nested_call_linter()] does not enforce +#' because it allows `aes()` as an argument. +#' +#' Both `ggplot()` and `ggplot2::ggplot()` are checked, +#' and arguments may be named (`data = df`, `mapping = aes(x, y)`) +#' or positional. +#' An `aes()` inside a layer such as `geom_point(aes(color = g))` +#' is not flagged. +#' +#' @return A [lintr::Linter()] object. +#' @export +ggplot_args_linter <- function() { + ggplot_call <- "//expr[expr[1]/SYMBOL_FUNCTION_CALL[text() = 'ggplot']]" + is_aes <- "expr[1]/SYMBOL_FUNCTION_CALL[text() = 'aes']" + aes_xpath <- paste0(ggplot_call, "/expr[position() > 1][", is_aes, "]") + data_xpath <- paste0(ggplot_call, "/expr[position() > 1][not(", is_aes, ")]") + + aes_message <- paste( + "Don't nest aes() inside ggplot(): pipe the data into ggplot()", + "and add the mapping as its own layer (`+ aes(x, y)`)." + ) + data_message <- paste( + "Don't pass data to ggplot(): pipe the primary data into a bare", + "ggplot() (`df |> ggplot() + aes(x, y)`)." + ) + + lintr::Linter(function(source_expression) { + if (!lintr::is_lint_level(source_expression, "expression")) { + return(list()) + } + + xml <- source_expression$xml_parsed_content + aes_nodes <- xml2::xml_find_all(xml, aes_xpath) + data_nodes <- xml2::xml_find_all(xml, data_xpath) + + aes_lints <- lintr::xml_nodes_to_lints( + aes_nodes, + source_expression = source_expression, + lint_message = aes_message, + type = "warning" + ) + data_lints <- lintr::xml_nodes_to_lints( + data_nodes, + source_expression = source_expression, + lint_message = data_message, + type = "warning" + ) + c(aes_lints, data_lints) + }) +} diff --git a/lms/README.md b/lms/README.md index d21cc929..c51826d9 100644 --- a/lms/README.md +++ b/lms/README.md @@ -32,7 +32,7 @@ It exports: and for tidyselect helpers, `aes()` and `vars()`, it flags nothing. The second flags an `if()` or `while()` condition that is not already a bare name or `TRUE`/`FALSE`, as in `while (TRUE)`. - `ggplot_aes_linter()` is the third: it flags `aes()` written inside `ggplot()`, + `ggplot_args_linter()` is the third: it flags any argument of `ggplot()` (`aes()` or the data), so pipe the data into a bare `ggplot()` and add the mapping as its own layer (`df |> ggplot() + aes(x, y)`). All three are off by default while existing code is swept; turn them on with `lms::default_linters(nested_calls = TRUE)`. diff --git a/lms/man/default_linters.Rd b/lms/man/default_linters.Rd index 21446079..0acfe30c 100644 --- a/lms/man/default_linters.Rd +++ b/lms/man/default_linters.Rd @@ -8,7 +8,7 @@ default_linters(nested_calls = FALSE) } \arguments{ \item{nested_calls}{Whether to include \code{\link[=nested_call_linter]{nested_call_linter()}}, -\code{\link[=bare_condition_linter]{bare_condition_linter()}} and \code{\link[=ggplot_aes_linter]{ggplot_aes_linter()}}, +\code{\link[=bare_condition_linter]{bare_condition_linter()}} and \code{\link[=ggplot_args_linter]{ggplot_args_linter()}}, the no-nested-calls policy. Off by default while existing code is swept; turn them on in a repo's \code{.lintr.R} with \code{lms::default_linters(nested_calls = TRUE)}.} diff --git a/lms/man/ggplot_aes_linter.Rd b/lms/man/ggplot_aes_linter.Rd deleted file mode 100644 index 9542be8d..00000000 --- a/lms/man/ggplot_aes_linter.Rd +++ /dev/null @@ -1,24 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ggplot_aes_linter.R -\name{ggplot_aes_linter} -\alias{ggplot_aes_linter} -\title{ggplot() aesthetic-mapping linter} -\usage{ -ggplot_aes_linter() -} -\value{ -A \code{\link[lintr:Linter]{lintr::Linter()}} object. -} -\description{ -Flags an \code{aes()} call written inside a call to \code{ggplot()}, as in -\code{ggplot(df, aes(x, y))}. Pipe the data into a bare \code{ggplot()} layer, -then add the mapping as its own layer: \code{df |> ggplot() + aes(x, y)}. -This is the no-nested-calls policy applied to \code{ggplot()}, -which \code{\link[=nested_call_linter]{nested_call_linter()}} does not enforce -because it allows \code{aes()} as an argument. -} -\details{ -Both \code{ggplot()} and \code{ggplot2::ggplot()} are checked, -and the mapping may be named (\code{mapping = aes(x, y)}) or positional. -An \code{aes()} inside a layer such as \code{geom_point(aes(color = g))} is not flagged. -} diff --git a/lms/man/ggplot_args_linter.Rd b/lms/man/ggplot_args_linter.Rd new file mode 100644 index 00000000..731a6f61 --- /dev/null +++ b/lms/man/ggplot_args_linter.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ggplot_args_linter.R +\name{ggplot_args_linter} +\alias{ggplot_args_linter} +\title{ggplot() arguments linter} +\usage{ +ggplot_args_linter() +} +\value{ +A \code{\link[lintr:Linter]{lintr::Linter()}} object. +} +\description{ +Flags any argument passed to \code{ggplot()}. +Pipe the primary data into a bare \code{ggplot()} layer, +then add the aesthetic mapping as its own layer: +\code{df |> ggplot() + aes(x, y)}, not \code{ggplot(df, aes(x, y))} +and not \code{ggplot(data = df)}. +This is the no-nested-calls policy applied to \code{ggplot()}, +which \code{\link[=nested_call_linter]{nested_call_linter()}} does not enforce +because it allows \code{aes()} as an argument. +} +\details{ +Both \code{ggplot()} and \code{ggplot2::ggplot()} are checked, +and arguments may be named (\code{data = df}, \code{mapping = aes(x, y)}) +or positional. +An \code{aes()} inside a layer such as \code{geom_point(aes(color = g))} +is not flagged. +} diff --git a/lms/tests/testthat/test-ggplot_aes_linter.R b/lms/tests/testthat/test-ggplot_aes_linter.R deleted file mode 100644 index 65971018..00000000 --- a/lms/tests/testthat/test-ggplot_aes_linter.R +++ /dev/null @@ -1,61 +0,0 @@ -message <- "Don't nest aes\\(\\) inside ggplot\\(\\)" - -test_that("aes() inside ggplot() is flagged", { - lintr::expect_lint( - "ggplot(df, aes(x, y))", - message, - ggplot_aes_linter() - ) - lintr::expect_lint( - "ggplot(aes(x, y))", - message, - ggplot_aes_linter() - ) -}) - -test_that("the namespaced and named forms are flagged", { - lintr::expect_lint( - "ggplot2::ggplot(df, ggplot2::aes(x, y))", - message, - ggplot_aes_linter() - ) - lintr::expect_lint( - "ggplot(df, mapping = aes(x, y))", - message, - ggplot_aes_linter() - ) - lintr::expect_lint( - "df |> ggplot2::ggplot(ggplot2::aes(x, y))", - message, - ggplot_aes_linter() - ) -}) - -test_that("aes() added as its own layer is not flagged", { - lintr::expect_no_lint( - "df |> ggplot2::ggplot() + ggplot2::aes(x, y)", - ggplot_aes_linter() - ) - lintr::expect_no_lint( - "fig_aes <- aes(x, y)\nggplot(df, fig_aes)", - ggplot_aes_linter() - ) - lintr::expect_no_lint("ggplot(df)", ggplot_aes_linter()) -}) - -test_that("aes() inside a layer is not flagged", { - lintr::expect_no_lint( - "ggplot(df, fig_aes) + geom_point(aes(colour = g))", - ggplot_aes_linter() - ) - lintr::expect_no_lint( - "ggplot2::geom_point(ggplot2::aes(colour = g))", - ggplot_aes_linter() - ) -}) - -test_that("default_linters(nested_calls = TRUE) includes it", { - linters <- default_linters(nested_calls = TRUE) - expect_true("ggplot_aes_linter" %in% names(linters)) - expect_false("ggplot_aes_linter" %in% names(default_linters())) -}) diff --git a/lms/tests/testthat/test-ggplot_args_linter.R b/lms/tests/testthat/test-ggplot_args_linter.R new file mode 100644 index 00000000..677ce790 --- /dev/null +++ b/lms/tests/testthat/test-ggplot_args_linter.R @@ -0,0 +1,80 @@ +aes_message <- "Don't nest aes\\(\\) inside ggplot\\(\\)" +data_message <- "Don't pass data to ggplot\\(\\)" + +test_that("aes() inside ggplot() is flagged", { + lintr::expect_lint( + "ggplot(aes(x, y))", + aes_message, + ggplot_args_linter() + ) + lintr::expect_lint( + "ggplot2::ggplot(ggplot2::aes(x, y))", + aes_message, + ggplot_args_linter() + ) + lintr::expect_lint( + "ggplot(mapping = aes(x, y))", + aes_message, + ggplot_args_linter() + ) + lintr::expect_lint( + "df |> ggplot2::ggplot(ggplot2::aes(x, y))", + aes_message, + ggplot_args_linter() + ) +}) + +test_that("data passed to ggplot() is flagged", { + lintr::expect_lint("ggplot(df)", data_message, ggplot_args_linter()) + lintr::expect_lint( + "ggplot(data = df)", + data_message, + ggplot_args_linter() + ) + lintr::expect_lint( + "ggplot2::ggplot(df) + aes(x, y)", + data_message, + ggplot_args_linter() + ) + lintr::expect_lint( + "ggplot(read_csv(path))", + data_message, + ggplot_args_linter() + ) +}) + +test_that("data and mapping together give one lint each", { + lints <- lintr::lint( + text = "ggplot(df, aes(x, y))", + linters = list(ggplot_args_linter = ggplot_args_linter()) + ) + messages <- vapply(lints, function(lint) lint$message, character(1)) + expect_match(messages[1], data_message) + expect_match(messages[2], aes_message) + expect_length(lints, 2) +}) + +test_that("the piped form with aes() as its own layer is not flagged", { + lintr::expect_no_lint( + "df |> ggplot2::ggplot() + ggplot2::aes(x, y)", + ggplot_args_linter() + ) + lintr::expect_no_lint("ggplot()", ggplot_args_linter()) +}) + +test_that("aes() inside a layer is not flagged", { + lintr::expect_no_lint( + "df |> ggplot() + aes(x, y) + geom_point(aes(colour = g))", + ggplot_args_linter() + ) + lintr::expect_no_lint( + "ggplot2::geom_point(ggplot2::aes(colour = g))", + ggplot_args_linter() + ) +}) + +test_that("default_linters(nested_calls = TRUE) includes it", { + linters <- default_linters(nested_calls = TRUE) + expect_true("ggplot_args_linter" %in% names(linters)) + expect_false("ggplot_args_linter" %in% names(default_linters())) +})