tests/testthat/test-sprintf_linter.R

patrick::with_parameters_test_that(
  "sprintf_linter skips allowed usages",
  {
    linter <- sprintf_linter()

    # NB: using paste0, not sprintf, to avoid escaping '%d' in sprint fmt=
    expect_lint(paste0(call_name, "('hello')"), NULL, linter)
    expect_lint(paste0(call_name, "('hello %d', 1)"), NULL, linter)
    expect_lint(paste0(call_name, "('hello %d', x)"), NULL, linter)
    expect_lint(paste0(call_name, "('hello %d', x + 1)"), NULL, linter)
    expect_lint(paste0(call_name, "('hello %d', f(x))"), NULL, linter)
    expect_lint(paste0(call_name, "('hello %1$s %1$s', x)"), NULL, linter)
    expect_lint(paste0(call_name, "('hello %1$s %1$s %2$d', x, y)"), NULL, linter)
    expect_lint(paste0(call_name, "('hello %1$s %1$s %2$d %3$s', x, y, 1.5)"), NULL, linter)
  },
  .test_name = c("sprintf", "gettextf"),
  call_name = c("sprintf", "gettextf")
)

patrick::with_parameters_test_that(
  "sprintf_linter blocks disallowed usages",
  {
    linter <- sprintf_linter()
    unused_arg_msg <- if (getRversion() >= "4.1.0") "one argument not used by format" else NULL

    expect_lint(paste0(call_name, "('hello', 1)"), unused_arg_msg, linter)

    expect_lint(
      paste0(call_name, "('hello %d', 'a')"),
      rex::rex("invalid format '%d'; use format %s for character objects"),
      linter
    )

    expect_lint(
      paste0(call_name, "('hello %d', 1.5)"),
      rex::rex("invalid format '%d'; use format %f, %e, %g or %a for numeric objects"),
      linter
    )

    expect_lint(
      paste0(call_name, "('hello %d',)"),
      rex::rex("argument is missing, with no default"),
      linter
    )

    expect_lint(paste0(call_name, "('hello %1$s %s', 'a', 'b')"), unused_arg_msg, linter)
    expect_lint(paste0(call_name, "('hello %1$s %1$s', x, y)"), unused_arg_msg, linter)

    expect_lint(
      paste0(call_name, "('hello %1$s %1$s %3$d', x, y)"),
      rex::rex("reference to non-existent argument 3"),
      linter
    )

    expect_lint(
      paste0(call_name, "('hello %1$s %1$s %2$d %3$d', x, y, 1.5)"),
      rex::rex("invalid format '%d'; use format %f, %e, %g or %a for numeric objects"),
      linter
    )
  },
  .test_name = c("sprintf", "gettextf"),
  call_name = c("sprintf", "gettextf")
)

test_that("edge cases are detected correctly", {
  linter <- sprintf_linter()

  # works with multi-line sprintf and comments
  expect_lint(
    trim_some("
      sprintf(
        'test fmt %s', # this is a comment
        2
      )
    "),
    NULL,
    linter
  )

  # dots
  expect_lint("sprintf('%d %d, %d', id, ...)", NULL, linter)

  # TODO(#1265) extend ... detection to at least test for too many arguments.

  # named argument fmt
  expect_lint("sprintf(x, fmt = 'hello %1$s %1$s')", NULL, linter)

  expect_lint(
    "sprintf(x, fmt = 'hello %1$s %1$s %3$d', y)",
    list(message = rex::rex("reference to non-existent argument 3")),
    linter
  )

  # #2131: xml2lang stripped necessary whitespace
  expect_lint("sprintf('%s', if (A) '' else y)", NULL, linter)
})

local({
  linter <- sprintf_linter()
  unused_fmt_msg <- "too few arguments"
  unused_arg_msg <- "one argument not used by format"
  pipes <- pipes(exclude = "%$%")
  patrick::with_parameters_test_that(
    "piping into sprintf works",
    {
      expect_lint(paste("x", pipe, "sprintf(fmt = '%s')"), NULL, linter)
      # no fmt= specified -> this is just 'sprintf("%s", "%s%s")', which won't lint
      expect_lint(paste('"%s"', pipe, 'sprintf("%s%s")'), NULL, linter)
      expect_lint(paste("x", pipe, "sprintf(fmt = '%s%s')"), unused_fmt_msg, linter)

      # Cannot evaluate statically --> skip
      expect_lint(paste("x", pipe, 'sprintf("a")'), NULL, linter)
      # Nested pipes
      expect_lint(
        paste("'%%sb'", pipe, "sprintf('%s')", pipe, "sprintf('a')"),
        if (getRversion() >= "4.1.0") list(column_number = nchar(paste("'%%sb'", pipe, "x")), message = unused_arg_msg),
        linter
      )
      expect_lint(
        paste("x", pipe, 'sprintf(fmt = "%s")', pipe, 'sprintf(fmt = "%s%s")'),
        list(column_number = nchar(paste("x", pipe, 'sprintf(fmt = "%s")', pipe, "x")), message = unused_fmt_msg),
        linter
      )
      expect_lint(
        paste("x", pipe, 'sprintf(fmt = "%s%s")', pipe, 'sprintf(fmt = "%s")'),
        list(column_number = nchar(paste("x", pipe, "x")), message = unused_fmt_msg),
        linter
      )
    },
    pipe = pipes,
    .test_name = names(pipes)
  )
})

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.