tests/testthat/test-object_usage_linter.R

test_that("returns the correct linting", {
  linter <- object_usage_linter()
  local_var_msg <- rex::rex("local variable", anything, "assigned but may not be used")

  expect_lint("blah", NULL, linter)

  expect_lint(
    trim_some("
      function() {
        a <- 1
        a
      }
    "),
    NULL,
    linter
  )

  expect_lint(
    trim_some("
      fun <- function(x) {
        fun(1)
      }
      fun2 <- function(x) {
        fun2(2)
      }
    "),
    NULL,
    linter
  )

  expect_lint(
    trim_some("
      fun <- function() {
        a <- 1
      }
    "),
    local_var_msg,
    linter
  )

  expect_lint(
    trim_some("
      fun <- function() {
        a <- 1
        1
      }
    "),
    local_var_msg,
    linter
  )

  expect_lint(
    trim_some("
      fun <- function() {
        a <- 1
      }
    "),
    local_var_msg,
    linter
  )

  # same, using = for assignment
  expect_lint(
    trim_some("
      fun = function() {
        a = 1
      }
    "),
    local_var_msg,
    linter
  )

  expect_lint(
    trim_some("
      fun <- function() {
        a2 <- 1
        a3
      }
    "),
    list(
      local_var_msg,
      rex::rex("no visible binding for global variable ", anything)
    ),
    linter
  )

  expect_lint(
    trim_some("
      fun <- function() {
        fnu(1)
      }
    "),
    rex::rex("no visible global function definition for ", anything),
    linter
  )

  # earlier we used n(1) but this might conflict with dplyr::n(),
  #   so switch to use an obscure symbol
  expect_lint(
    trim_some("
      fun <- function(x) {
        `__lintr_obj`(1)
      }
    "),
    rex::rex("no visible global function definition for ", anything),
    linter
  )

  # setMethod and assign also checked

  expect_lint(
    trim_some("
      assign('fun', function() {
        a <- 1
        1
      })
    "),
    local_var_msg,
    linter
  )

  expect_lint(
    trim_some("
      setMethod('plot', 'numeric', function() {
        a <- 1
        1
      })
    "),
    local_var_msg,
    linter
  )
})

test_that("replace_functions_stripped", {
  expect_lint(
    trim_some("
      fun <- function(x) {
        `__lintr_obj`(x) = 1
      }
    "),
    rex::rex("no visible global function definition for ", anything),
    object_usage_linter()
  )

  expect_lint(
    trim_some("
      fun <- function(x) {
        `__lintr_obj`(x) <- 1
      }
    "),
    rex::rex("no visible global function definition for ", anything),
    object_usage_linter()
  )
})

test_that("eval errors are ignored", {
  expect_lint(
    trim_some('
    setMethod("[[<-", c("stampedEnv", "character", "missing"),
      function(x) {
        x
      })
    '),
    NULL,
    object_usage_linter()
  )
})

test_that("calls with top level function definitions are ignored", {
  expect_lint(
    'tryCatch("foo", error = function(e) e)',
    NULL,
    object_usage_linter()
  )
})

test_that("object-usage line-numbers are relative to start-of-file", {
  expect_lint(
    trim_some("
      a <- function(y) {
        y ** 2
      }
      b <- function() {
        x
      }
    "),
    list(line_number = 5L),
    object_usage_linter()
  )
})

test_that("used symbols are detected correctly", {
  # From #666
  expect_lint(
    trim_some('
      foo <- data.frame(0)
      foo$bar <- 1
      zero <- function() {
        file.info("/dev/null")$size
      }
      message(zero())
    '),
    NULL,
    object_usage_linter()
  )

  expect_lint(
    trim_some("
      foo$bar <- 1
      zero <- function() {
        foo
      }
      message(zero())
    "),
    list("foo"),
    object_usage_linter()
  )

  # Also test deeper nesting
  expect_lint(
    trim_some('
      foo <- list(0)
      foo$bar$baz$goo <- 1
      zero <- function() {
        file.info("/dev/null")$size
        foo$bar
        foo$bar$baz
        foo$bar$baz$goo
      }
      message(zero())
    '),
    NULL,
    object_usage_linter()
  )

  # Test alternative assignment and access methods
  expect_lint(
    trim_some('
      foo <- list(0)
      foo[["bar"]][["baz"]][["goo"]] <- 1
      zero <- function() {
        file.info("/dev/null")$size
        foo$bar
        foo$bar$baz
        foo$bar$baz$goo
        foo[["bar"]]
        foo[[c("bar", "baz")]]
        foo[["bar"]]$baz$goo
      }
      message(zero())
    '),
    NULL,
    object_usage_linter()
  )



  # regression #1322
  expect_silent(expect_lint("assign('x', 42)", NULL, object_usage_linter()))
})

test_that("object_usage_linter finds lints spanning multiple lines", {
  # Regression test for #507
  expect_lint(
    trim_some("
      foo <- function() {
        if (unknown_function()) NULL

        if (unknown_function()) {
          NULL
        }
      }
    "),
    list(
      list(message = "unknown_function", line_number = 2L),
      list(message = "unknown_function", line_number = 4L)
    ),
    object_usage_linter()
  )

  # Linted symbol is not on the first line of the usage warning
  expect_lint(
    trim_some("
      foo <- function(x) {
        with(
          x,
          unknown_symbol
        )
      }
    "),
    list(message = "unknown_symbol", line_number = 4L, column_number = 5L),
    object_usage_linter(skip_with = FALSE)
  )

  # Even ugly names are found
  expect_lint(
    trim_some("
      foo <- function(x) {
        with(
          x,
          `\u2019regex_kill`
        )
      }
    "),
    list(line_number = 4L, column_number = 5L),
    object_usage_linter(skip_with = FALSE)
  )
})

test_that("global variable detection works", {
  old_globals <- utils::globalVariables(package = globalenv())
  utils::globalVariables("global_function", package = globalenv())
  on.exit(utils::globalVariables(old_globals, package = globalenv(), add = FALSE))

  expect_lint(
    trim_some("
      foo <- function() {
        if (global_function()) NULL

        if (global_function()) {
          NULL
        }
      }
    "),
    NULL,
    object_usage_linter()
  )
})

test_that("package detection works", {
  expect_length(
    lint_package(test_path("dummy_packages", "package"), linters = object_usage_linter(), parse_settings = FALSE),
    10L
  )
})

test_that("robust against errors", {
  expect_lint(
    'assign("x", unknown_function)',
    NULL,
    object_usage_linter()
  )
})

test_that("interprets glue expressions", {
  linter <- object_usage_linter()

  expect_lint(trim_some("
    fun <- function() {
      local_var <- 42
      glue::glue('The answer is {local_var}.')
    }
  "), NULL, linter)

  # no need for namespace-qualification
  expect_lint(trim_some("
    glue <- glue::glue # imitate this being an @import
    fun <- function() {
      local_var <- 42
      glue('The answer is {local_var}.')
    }
  "), NULL, linter)

  # multiple variables in different interpolations
  expect_lint(trim_some("
    fun <- function() {
      local_key <- 'a'
      local_value <- 123
      glue::glue('Key-value pair: {local_key}={local_value}.')
    }
  "), NULL, linter)

  # multiple variables in single interpolation
  expect_lint(trim_some("
    fun <- function() {
      local_str1 <- 'a'
      local_str2 <- 'b'
      glue::glue('With our powers combined: {paste(local_str1, local_str2)}.')
    }
  "), NULL, linter)

  # Check non-standard .open and .close
  expect_lint(trim_some("
    fun <- function() {
      local_var <- 42
      glue::glue('The answer is $[local_var].', .open = '$[', .close = ']')
    }
  "), NULL, linter)

  # Steer clear of custom .transformer and .envir constructs
  expect_lint(trim_some("
    fun <- function() {
      local_var <- 42
      glue::glue('The answer is {local_var}.', .transformer = glue::identity_transformer)
    }
  "), "local_var", linter)

  expect_lint(trim_some("
    fun <- function() {
      local_var <- 42
      e <- new.env()
      glue::glue('The answer is {local_var}.', .envir = e)
    }
  "), "local_var", linter)

  # unused is caught, glue-used is not
  expect_lint(trim_some("
    fun <- function() {
      local_var <- 42
      unused_var <- 3
      glue::glue('The answer is {local_var}.')
    }
  "), "unused_var", linter)

  # glue-only is caught with option off
  expect_lint(trim_some("
    fun <- function() {
      local_var <- 42
      glue::glue('The answer is {local_var}.')
    }
  "), "local_var", object_usage_linter(interpret_glue = FALSE))

  # call in glue is caught
  expect_lint(
    trim_some("
      fun <- function() {
        local_call <- identity
        local_unused_call <- identity
        glue::glue('{local_call(1)}')
      }
    "),
    "local_unused_call",
    linter
  )

  # ditto infix operator
  expect_lint(trim_some("
    glue <- glue::glue # imitate this being an @import
    foo <- function() {
      `%++%` <- `+`
      glue('{x %++% y}')
    }
  "), NULL, linter)
})

test_that("errors/edge cases in glue syntax don't fail lint()", {
  linter <- object_usage_linter()

  # no lint & no error, despite glue error
  expect_warning(
    expect_lint(
      trim_some("
        fun <- function() {
          a <- 2
          a + 1
          glue::glue('The answer is {a')
        }
      "),
      NULL,
      linter
    ),
    "Evaluating glue expression.*failed: Expecting '\\}'.*Please ensure correct glue syntax"
  )

  # generates a lint because the "usage" inside glue() is not detected
  expect_warning(
    expect_lint(
      trim_some("
        fun <- function() {
          a <- 2
          glue::glue('The answer is {a')
        }
      "),
      "local variable 'a'",
      linter
    ),
    "Evaluating glue expression.*failed: Expecting '\\}'"
  )

  # complete {...}, but syntax error in code -> ignore
  expect_lint(
    trim_some("
      fun <- function() {
        a <- 2
        glue::glue('The answer is {a + }')
      }
    "),
    "local variable 'a'",
    linter
  )

  # empty glue expression {}
  expect_lint(
    trim_some("
      fun <- function() {
        a <- 2
        glue::glue('The answer is {}: {a}')
      }
    "),
    NULL,
    linter
  )

  # comment inside glue range (#1919)
  expect_lint(
    trim_some("
      fun <- function() {
        a <- 2
        glue::glue(
          'The answer is {}: {a}' # show the answer
        )
      }
    "),
    NULL,
    linter
  )
})

test_that("backtick'd names in glue are handled", {
  expect_lint(
    trim_some("
      fun <- function() {
        `w` <- 2
        x <- 3
        y <- -4
        `\\`y` <- 4
        z <- -5
        `z\\`` <- 5
        glue::glue('{w}{`x`}{y}{z}')
      }
    "),
    list(
      rex::rex("local variable '`y'"),
      rex::rex("local variable 'z`'")
    ),
    object_usage_linter()
  )
})

# reported as #1088
test_that("definitions below top level are ignored (for now)", {
  expect_lint(
    trim_some("
      local({
        x <- 1
        f <- function() {
          x
        }
      })
    "),
    NULL,
    object_usage_linter()
  )
})

# reported as #1127
test_that("package imports are detected if present in file", {
  skip_if("package:xml2" %in% search())
  expect_lint(
    trim_some("
      dog <- function(url) {
        a <- read_xml(url)
        a
      }
    "),
    rex::rex("no visible global function definition for ", anything, "read_xml"),
    object_usage_linter()
  )

  expect_lint(
    trim_some("
      library(xml2)

      dog <- function(url) {
        a <- read_xml(url)
        a
      }
    "),
    NULL,
    object_usage_linter()
  )
})

test_that("fallback works", {
  expect_lint(
    trim_some("
      f <- function() {
        `non_existing_assign<-`(1, 2)
      }
    "),
    list(
      message = rex::rex("no visible global function definition for ", anything, "non_existing_assign<-"),
      line_number = 2L,
      column_number = 3L
    ),
    object_usage_linter()
  )
})

test_that("unknown infix operators give good lint metadata", {
  expect_lint(
    trim_some("
      foo <- function(x) {
        x %unknown-operator% 1
      }
    "),
    list(
      message = rex::rex("no visible global function definition for '%unknown-operator%'"),
      line_number = 2L, column_number = 5L
    ),
    object_usage_linter()
  )

  skip_if(any(c("package:rlang", "package:data.table") %in% search()))
  expect_lint(
    trim_some('
      foo <- function(x) {
        x[, "new_col" := 2L]
      }
    '),
    list(
      message = rex::rex("no visible global function definition for ':='"),
      line_number = 2L, column_number = 17L
    ),
    object_usage_linter()
  )
})

test_that("respects `skip_with` argument for `with()` expressions", {
  f <- withr::local_tempfile(
    lines = c(
      "test_fun <- function(df) {",
      "  with(df, first_var + second_var)",
      "}"
    )
  )

  expect_length(lint(f, object_usage_linter(skip_with = TRUE)), 0L)
  expect_length(lint(f, object_usage_linter(skip_with = FALSE)), 2L)
})

test_that("missing libraries don't cause issue", {
  expect_lint(
    trim_some("
      library(a.a.a.z.z.z)
      foo <- function() {
        a <- 1
        a
      }
    "),
    NULL,
    object_usage_linter()
  )
})

test_that("messages without a quoted name are caught", {
  # regression test for #1714
  expect_lint(
    trim_some("
      foo <- function() {
        a <- ...
        a
      }
    "),
    list(
      message = "... may be used in an incorrect context",
      line_number = 2L
    ),
    object_usage_linter()
  )
})

# See #1914
test_that("symbols in formulas aren't treated as 'undefined global'", {
  expect_lint(
    trim_some("
      foo <- function(x) {
        lm(
          y ~ z,
          data = x[!is.na(y)]
        )
      }
    "),
    list(
      message = "no visible binding for global variable 'y'",
      line_number = 4L,
      column_number = 21L
    ),
    object_usage_linter()
  )

  # neither on the RHS
  expect_lint(
    trim_some("
      foo <- function(x) {
        lm(
          z ~ y,
          data = x[!is.na(y)]
        )
      }
    "),
    list(
      message = "no visible binding for global variable 'y'",
      line_number = 4L,
      column_number = 21L
    ),
    object_usage_linter()
  )

  # nor in nested expressions
  expect_lint(
    trim_some("
      foo <- function(x) {
        lm(
          log(log(y)) ~ z,
          data = x[!is.na(y)]
        )
      }
    "),
    list(
      message = "no visible binding for global variable 'y'",
      line_number = 4L,
      column_number = 21L
    ),
    object_usage_linter()
  )

  # nor as a call
  # NB: I wanted this to be s(), as in mgcv::s(), but that
  #   doesn't work in this test suite because it resolves to
  #   rex::s() since we attach that in testthat.R
  expect_lint(
    trim_some("
      foo <- function(x) {
        lm(
          y(w) ~ z,
          data = x[!is.na(y)]
        )
      }
    "),
    list(
      message = "no visible binding for global variable 'y'",
      line_number = 4L,
      column_number = 21L
    ),
    object_usage_linter()
  )
})

test_that("NSE-ish symbols after $/@ are ignored as sources for lints", {
  expect_lint(
    trim_some("
      foo <- function(x) {
        ggplot2::ggplot(
          x[!is.na(x$column), ],
          ggplot2::aes(x = column, fill = factor(x$grp))
        )
      }
    "),
    list(
      message = "no visible binding for global variable 'column'",
      line_number = 4L,
      column_number = 22L
    ),
    object_usage_linter()
  )

  expect_lint(
    trim_some("
      foo <- function(x) {
        ggplot2::ggplot(
          x[!is.na(x@column), ],
          ggplot2::aes(x = column, fill = factor(x$grp))
        )
      }
    "),
    list(
      message = "no visible binding for global variable 'column'",
      line_number = 4L,
      column_number = 22L
    ),
    object_usage_linter()
  )
})

test_that("functional lambda definitions are also caught", {
  skip_if_not_r_version("4.1.0")

  expect_lint(
    trim_some("
      fun <- \\() {
        a <- 1
      }
    "),
    rex::rex("local variable", anything, "assigned but may not be used"),
    object_usage_linter()
  )
})

test_that("messages without location info are repaired", {
  # regression test for #1986
  expect_lint(
    trim_some("
      foo <- function() no_fun()
    "),
    list(
      message = rex::rex("no visible global function definition for", anything),
      line_number = 1L,
      column_number = 19L
    ),
    object_usage_linter()
  )

  expect_lint(
    trim_some("
      foo <- function(a = no_fun()) a
    "),
    list(
      message = rex::rex("no visible global function definition for", anything),
      line_number = 1L,
      column_number = 21L
    ),
    object_usage_linter()
  )

  expect_lint(
    trim_some("
      foo <- function() no_global
    "),
    list(
      message = rex::rex("no visible binding for global variable", anything),
      line_number = 1L,
      column_number = 19L
    ),
    object_usage_linter()
  )

  expect_lint(
    trim_some("
      foo <- function() unused_local <- 42L
    "),
    list(
      message = rex::rex("local variable", anything, "assigned but may not be used"),
      line_number = 1L,
      column_number = 19L
    ),
    object_usage_linter()
  )

  # More complex case with two lints and missing location info
  expect_lint(
    trim_some("
      foo <- function() a <-
        bar()
    "),
    list(
      list(
        message = rex::rex("local variable", anything, "assigned but may not be used"),
        line_number = 1L,
        column_number = 19L
      ),
      list(
        message = rex::rex("no visible global function definition for", anything),
        line_number = 2L,
        column_number = 3L
      )
    ),
    object_usage_linter()
  )
})

Try the lintr package in your browser

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

lintr documentation built on Nov. 7, 2023, 5:07 p.m.