Skip to content

Commit b560662

Browse files
authored
warn about discouraged $ and [[ usage in usage within aes() (#3346, fixes #2693)
2 parents 6fbe27c + 918103b commit b560662

File tree

4 files changed

+115
-11
lines changed

4 files changed

+115
-11
lines changed

R/aes.r

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ NULL
88
#' [ggplot2()] and in individual layers.
99
#'
1010
#' This function also standardises aesthetic names by converting `color` to `colour`
11-
#' (also in substrings, e.g. `point_color` to `point_colour`) and translating old style
12-
#' R names to ggplot names (eg. `pch` to `shape`, `cex` to `size`).
11+
#' (also in substrings, e.g., `point_color` to `point_colour`) and translating old style
12+
#' R names to ggplot names (e.g., `pch` to `shape` and `cex` to `size`).
1313
#'
1414
#' @section Quasiquotation:
1515
#'
@@ -22,9 +22,13 @@ NULL
2222
#' programming vignette](http://dplyr.tidyverse.org/articles/programming.html)
2323
#' to learn more about these techniques.
2424
#'
25-
#' @param x,y,... List of name value pairs giving aesthetics to map to
26-
#' variables. The names for x and y aesthetics are typically omitted because
27-
#' they are so common; all other aesthetics must be named.
25+
#' @param x,y,... List of name-value pairs in the form `aesthetic = variable`
26+
#' describing which variables in the layer data should be mapped to which
27+
#' aesthetics used by the paired geom/stat. The expression `variable` is
28+
#' evaluated within the layer data, so there is no need to refer to
29+
#' the original dataset (i.e., use `ggplot(df, aes(variable))`
30+
#' instead of `ggplot(df, aes(df$variable))`). The names for x and y aesthetics
31+
#' are typically omitted because they are so common; all other aesthetics must be named.
2832
#' @seealso [vars()] for another quoting function designed for
2933
#' faceting specifications.
3034
#' @return A list with class `uneval`. Components of the list are either
@@ -334,3 +338,55 @@ mapped_aesthetics <- function(x) {
334338
is_null <- vapply(x, is.null, logical(1))
335339
names(x)[!is_null]
336340
}
341+
342+
343+
#' Check a mapping for discouraged usage
344+
#'
345+
#' Checks that `$` and `[[` are not used when the target *is* the data
346+
#'
347+
#' @param mapping A mapping created with [aes()]
348+
#' @param data The data to be mapped from
349+
#'
350+
#' @noRd
351+
warn_for_aes_extract_usage <- function(mapping, data) {
352+
lapply(mapping, function(quosure) {
353+
warn_for_aes_extract_usage_expr(get_expr(quosure), data, get_env(quosure))
354+
})
355+
}
356+
357+
warn_for_aes_extract_usage_expr <- function(x, data, env = emptyenv()) {
358+
if (is_call(x, "[[") || is_call(x, "$")) {
359+
if (extract_target_is_likely_data(x, data, env)) {
360+
good_usage <- alternative_aes_extract_usage(x)
361+
warning(
362+
"Use of `", format(x), "` is discouraged. ",
363+
"Use `", good_usage, "` instead.",
364+
call. = FALSE
365+
)
366+
}
367+
} else if (is.call(x)) {
368+
lapply(x, warn_for_aes_extract_usage_expr, data, env)
369+
}
370+
}
371+
372+
alternative_aes_extract_usage <- function(x) {
373+
if (is_call(x, "[[")) {
374+
good_call <- call2("[[", quote(.data), x[[3]])
375+
format(good_call)
376+
} else if (is_call(x, "$")) {
377+
as.character(x[[3]])
378+
} else {
379+
stop("Don't know how to get alternative usage for `", format(x), "`", call. = FALSE)
380+
}
381+
}
382+
383+
extract_target_is_likely_data <- function(x, data, env) {
384+
if (!is.name(x[[2]])) {
385+
return(FALSE)
386+
}
387+
388+
tryCatch({
389+
data_eval <- eval_tidy(x[[2]], data, env)
390+
identical(data_eval, data)
391+
}, error = function(err) FALSE)
392+
}

R/layer.r

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,14 @@ Layer <- ggproto("Layer", NULL,
238238

239239
scales_add_defaults(plot$scales, data, aesthetics, plot$plot_env)
240240

241-
# Evaluate and check aesthetics
241+
# Evaluate aesthetics
242242
evaled <- lapply(aesthetics, eval_tidy, data = data)
243243
evaled <- compact(evaled)
244244

245+
# Check for discouraged usage in mapping
246+
warn_for_aes_extract_usage(aesthetics, data[setdiff(names(data), "PANEL")])
247+
248+
# Check aesthetic values
245249
nondata_cols <- check_nondata_cols(evaled)
246250
if (length(nondata_cols) > 0) {
247251
msg <- paste0(

man/aes.Rd

Lines changed: 9 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-aes.r

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,46 @@ test_that("aes standardises aesthetic names", {
111111
expect_warning(aes(color = x, colour = y), "Duplicated aesthetics")
112112
})
113113

114+
test_that("warn_for_aes_extract_usage() warns for discouraged uses of $ and [[ within aes()", {
115+
116+
df <- data_frame(x = 1:5, nested_df = data_frame(x = 6:10))
117+
118+
expect_warning(
119+
warn_for_aes_extract_usage(aes(df$x), df),
120+
"Use of `df\\$x` is discouraged"
121+
)
122+
123+
expect_warning(
124+
warn_for_aes_extract_usage(aes(df[["x"]]), df),
125+
'Use of `df\\[\\["x"\\]\\]` is discouraged'
126+
)
127+
})
128+
129+
test_that("warn_for_aes_extract_usage() does not evaluate function calls", {
130+
df <- data_frame(x = 1:5, nested_df = data_frame(x = 6:10))
131+
returns_df <- function() df
132+
133+
expect_warning(warn_for_aes_extract_usage(aes(df$x), df))
134+
expect_silent(warn_for_aes_extract_usage(aes(returns_df()$x), df))
135+
})
136+
137+
test_that("warn_for_aes_extract_usage() does not warn for valid uses of $ and [[ within aes()", {
138+
df <- data_frame(x = 1:5, nested_df = data_frame(x = 6:10))
139+
140+
# use of .data
141+
expect_silent(warn_for_aes_extract_usage(aes(.data$x), df))
142+
expect_silent(warn_for_aes_extract_usage(aes(.data[["x"]]), df))
143+
144+
# use of $ for a nested data frame column
145+
expect_silent(warn_for_aes_extract_usage(aes(nested_df$x), df))
146+
expect_silent(warn_for_aes_extract_usage(aes(nested_df[["x"]]), df))
147+
})
148+
149+
test_that("Warnings are issued when plots use discouraged extract usage within aes()", {
150+
df <- data_frame(x = 1:3, y = 1:3)
151+
p <- ggplot(df, aes(df$x, y)) + geom_point()
152+
expect_warning(ggplot_build(p), "Use of `df\\$x` is discouraged")
153+
})
114154

115155
# Visual tests ------------------------------------------------------------
116156

0 commit comments

Comments
 (0)