File: test-sprintf_linter.R

package info (click to toggle)
r-cran-lintr 3.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 4,396 kB
  • sloc: sh: 13; xml: 10; makefile: 2
file content (149 lines) | stat: -rw-r--r-- 4,757 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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)
  )
})

test_that("lints vectorize", {
  skip_if_not_r_version("4.1.0")

  expect_lint(
    trim_some("{
      sprintf('%s', a, b)
      sprintf('%s%s', a)
    }"),
    list(
      list("one argument not used by format", line_number = 2L),
      list("too few arguments", line_number = 3L)
    ),
    sprintf_linter()
  )
})