R/helper_check_lhs.R

Defines functions check_lhs_is_names

# =============================================================================
# helper_check_lhs.R
# =============================================================================
# Internal helper. Warns when the LHS of a formula contains expressions
# (function calls, arithmetic) instead of bare variable names.
#
# Why this guard exists
# ---------------------
# rfriend's formula-based functions (f_aov, f_glm, f_kruskal_test, f_lmer,
# f_t_test, f_wilcox_test, f_summary, f_outliers, f_scan, f_stat_wizard,
# f_boxplot) all parse the LHS via:
#
#     lhs <- all.vars(formula[[2]])
#
# all.vars() silently strips function calls. That means:
#
#   log(y)  ~ group   -> LHS extracted as "y" (untransformed!)
#   I(y^2)  ~ group   -> LHS extracted as "y"
#   sqrt(y) ~ group   -> LHS extracted as "y"
#
# In every case the function then operates on the untransformed column,
# producing a correct-looking analysis of the wrong data. This warning
# alerts the user so they can precompute a column and use its name.
#
# Allowed syntax (no warning)
# ---------------------------
#   y ~ group                        bare name
#   y1 + y2 ~ group                  multiple responses (rfriend convention)
#   y1 - y2 ~ group                  same; minus also treated as a separator
#   y1 + y2 + y3 ~ group             any number of responses
#   `my column` ~ group              backtick-quoted non-syntactic name
#   y1 + `my column` ~ group         mix of bare and backticked names
#
# Disallowed syntax (warning fires, naming the offending term)
# -----------------------------------------------------------
#   log(y) ~ group                   function call on LHS
#   I(y^2) ~ group                   I() wrapper on LHS
#   sqrt(y1) + y2 ~ group            one call + one bare name -> warns on sqrt(y1)
#   y * 2 ~ group                    arithmetic on LHS
#
# The warning is emitted with call. = FALSE so that users see a clean
# message without an unhelpful "Error in check_lhs_is_names(...)" prefix.
# =============================================================================

check_lhs_is_names <- function(formula) {

  # Nothing to check when no formula was supplied. Several rfriend
  # functions accept either a formula or a bare data.frame (f_boxplot,
  # f_summary, f_scan, f_outliers, ...). In the data.frame-only path
  # 'formula' is NULL; deparsing NULL would produce the literal string
  # "NULL", which then fails the bare-name test and emits a misleading
  # warning naming NULL as an offending LHS term.
  if (is.null(formula)) {
    return(invisible(NULL))
  }

  # Some callers may also pass a one-sided formula (e.g. ~ group) with
  # no LHS at all. length(formula) is 2 in that case (operator + RHS);
  # there is nothing to validate, so return early.
  if (length(formula) < 3L) {
    return(invisible(NULL))
  }

  # Deparse the LHS back to source text. Split on '+' to recover the
  # individual response terms as rfriend would see them.
  lhs_deparsed <- deparse(formula[[2]])
  lhs_terms <- trimws(strsplit(lhs_deparsed, "[+\\-]")[[1]])

  # Strip surrounding backticks from a term, if present.
  # A backtick-quoted name like `my column` is syntactically a valid
  # reference, so it counts as a bare name for the purpose of this check.
  strip_backticks <- function(t) {
    if (startsWith(t, "`") && endsWith(t, "`")) {
      substr(t, 2L, nchar(t) - 1L)
    } else {
      t
    }
  }

  # A term qualifies as a bare (or backticked) name if:
  #   - after stripping backticks the inner text is a syntactically
  #     valid R name (make.names() does not alter it), OR
  #   - the term as written already matches make.names() unchanged.
  #
  # The two-path check means both plain names ("y1") and backticked
  # non-syntactic names ("`my column`") pass cleanly, while function
  # calls like "log(y)" fail because make.names("log(y)") returns
  # "log.y." which differs from the original.
  is_bare <- vapply(lhs_terms, function(t) {
    if (startsWith(t, "`") && endsWith(t, "`")) {
      # Backtick-wrapped: the wrapping itself is syntactic; the inner
      # contents are allowed to be anything, so this always passes.
      return(TRUE)
    }
    t == make.names(t)
  }, logical(1))

  if (!all(is_bare)) {
    bad <- lhs_terms[!is_bare]
    warning(
      "Expressions on the LHS of the formula are ignored: ",
      paste(bad, collapse = ", "),
      ". \nOnly variable names are used by rfriend. \nPrecompute a column in `data` ",
      "instead (e.g. `data$log_value <- log(data$value)`).",
      call. = FALSE
    )
  }

  invisible(NULL)
}

Try the rfriend package in your browser

Any scripts or data that you put into this service are public.

rfriend documentation built on July 7, 2026, 1:06 a.m.