Skip to content

Commit 60d43a6

Browse files
Merge branch 'main' into unsorted
2 parents 9289337 + 3d9e6d7 commit 60d43a6

21 files changed

+274
-50
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ importFrom(rex,rex)
155155
importFrom(stats,na.omit)
156156
importFrom(utils,capture.output)
157157
importFrom(utils,getParseData)
158+
importFrom(utils,globalVariables)
158159
importFrom(utils,head)
159160
importFrom(utils,relist)
160161
importFrom(utils,tail)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* `sprintf_linter()` is pipe-aware, so that `x %>% sprintf(fmt = "%s")` no longer lints (#1943, @MichaelChirico).
2525
* `line_length_linter()` helpfully includes the line length in the lint message (#2057, @MichaelChirico).
2626
* `sort_linter()` checks for code like `x == sort(x)` which is better served by using the function `is.unsorted()` (part of #884, @MichaelChirico).
27+
* `paste_linter()` gains detection for file paths that are better constructed with `file.path()`, e.g. `paste0(dir, "/", file)` would be better as `file.path(dir, file)` (part of #884, @MichaelChirico).
2728

2829
### New linters
2930

R/aaa.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ NULL
2121
# need to register rex shortcuts as globals to avoid CRAN check errors
2222
rex::register_shortcuts("lintr")
2323

24-
utils::globalVariables(
24+
globalVariables(
2525
c(
2626
"line1", "col1", "line2", "col2", # columns of parsed_content
2727
"id", "parent", "token", "terminal", "text" # ditto

R/comment_linters.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ commented_code_linter <- function() {
8484
code_candidates <- re_matches(all_comments, code_candidate_regex, global = FALSE, locations = TRUE)
8585
extracted_code <- code_candidates[, "code"]
8686
# ignore trailing ',' when testing for parsability
87-
extracted_code <- rex::re_substitutes(extracted_code, rex::rex(",", any_spaces, end), "")
88-
extracted_code <- rex::re_substitutes(extracted_code, rex::rex(start, any_spaces, ","), "")
87+
extracted_code <- re_substitutes(extracted_code, rex(",", any_spaces, end), "")
88+
extracted_code <- re_substitutes(extracted_code, rex(start, any_spaces, ","), "")
8989
is_parsable <- which(vapply(extracted_code, parsable, logical(1L)))
9090

9191
lint_list <- xml_nodes_to_lints(

R/exclude.R

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ parse_exclusions <- function(file, exclude = settings$exclude,
110110
return(list())
111111
}
112112

113-
start_locations <- rex::re_matches(lines, exclude_start, locations = TRUE)[, "end"] + 1L
114-
end_locations <- rex::re_matches(lines, exclude_end, locations = TRUE)[, "start"]
113+
start_locations <- re_matches(lines, exclude_start, locations = TRUE)[, "end"] + 1L
114+
end_locations <- re_matches(lines, exclude_end, locations = TRUE)[, "start"]
115115
starts <- which(!is.na(start_locations))
116116
ends <- which(!is.na(end_locations))
117117

@@ -125,20 +125,20 @@ parse_exclusions <- function(file, exclude = settings$exclude,
125125
for (i in seq_along(starts)) {
126126
excluded_lines <- seq(starts[i], ends[i])
127127
linters_string <- substring(lines[starts[i]], start_locations[starts[i]])
128-
linters_string <- rex::re_matches(linters_string, exclude_linter)[, 1L]
128+
linters_string <- re_matches(linters_string, exclude_linter)[, 1L]
129129

130130
exclusions <- add_exclusions(exclusions, excluded_lines, linters_string, exclude_linter_sep, linter_names)
131131
}
132132
}
133133

134-
nolint_locations <- rex::re_matches(lines, exclude, locations = TRUE)[, "end"] + 1L
134+
nolint_locations <- re_matches(lines, exclude, locations = TRUE)[, "end"] + 1L
135135
nolints <- which(!is.na(nolint_locations))
136136
# Disregard nolint tags if they also match nolint start / end
137137
nolints <- setdiff(nolints, c(starts, ends))
138138

139139
for (i in seq_along(nolints)) {
140140
linters_string <- substring(lines[nolints[i]], nolint_locations[nolints[i]])
141-
linters_string <- rex::re_matches(linters_string, exclude_linter)[, 1L]
141+
linters_string <- re_matches(linters_string, exclude_linter)[, 1L]
142142
exclusions <- add_exclusions(exclusions, nolints[i], linters_string, exclude_linter_sep, linter_names)
143143
}
144144

R/extract.R

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,16 @@ defines_knitr_engine <- function(start_lines) {
137137
engines <- names(knitr::knit_engines$get())
138138

139139
# {some_engine}, {some_engine label, ...} or {some_engine, ...}
140-
bare_engine_pattern <- rex::rex(
140+
bare_engine_pattern <- rex(
141141
"{", or(engines), one_of("}", " ", ",")
142142
)
143143
# {... engine = "some_engine" ...}
144-
explicit_engine_pattern <- rex::rex(
144+
explicit_engine_pattern <- rex(
145145
boundary, "engine", any_spaces, "="
146146
)
147147

148-
rex::re_matches(start_lines, explicit_engine_pattern) |
149-
rex::re_matches(start_lines, bare_engine_pattern)
148+
re_matches(start_lines, explicit_engine_pattern) |
149+
re_matches(start_lines, bare_engine_pattern)
150150
}
151151

152152
replace_prefix <- function(lines, prefix_pattern) {

R/get_source_expressions.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ fixup_line <- function(line) {
185185
#'
186186
#' @noRd
187187
lint_parse_error_r43 <- function(e, source_expression) {
188-
msg <- rex::re_substitutes(e$message, rex::rex(" (", except_some_of(")"), ")", end), "")
188+
msg <- re_substitutes(e$message, rex(" (", except_some_of(")"), ")", end), "")
189189
line_number <- e$lineno
190190
column <- e$colno
191191
substr(msg, 1L, 1L) <- toupper(substr(msg, 1L, 1L))
@@ -621,7 +621,7 @@ fix_eq_assigns <- function(pc) {
621621

622622
eq_assign_locs <- which(pc$token == "EQ_ASSIGN")
623623
# check whether the equal-assignment is the final entry
624-
if (length(eq_assign_locs) == 0L || utils::tail(eq_assign_locs, 1L) == nrow(pc)) {
624+
if (length(eq_assign_locs) == 0L || tail(eq_assign_locs, 1L) == nrow(pc)) {
625625
return(pc)
626626
}
627627

R/indentation_linter.R

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ indentation_linter <- function(indent = 2L, hanging_indent_style = c("tidy", "al
189189
({xp_or(paste0('descendant::', paren_tokens_left, '[', xp_last_on_line, ']'))})
190190
]/@line1
191191
)]"),
192-
glue::glue("({ global_nodes(infix_tokens) })[{xp_last_on_line}{infix_condition}]"),
193-
glue::glue("({ global_nodes(no_paren_keywords) })[{xp_last_on_line}]"),
194-
glue::glue("
192+
glue("({ global_nodes(infix_tokens) })[{xp_last_on_line}{infix_condition}]"),
193+
glue("({ global_nodes(no_paren_keywords) })[{xp_last_on_line}]"),
194+
glue("
195195
({ global_nodes(keyword_tokens) })
196196
/following-sibling::OP-RIGHT-PAREN[
197197
{xp_last_on_line} and
@@ -223,9 +223,9 @@ indentation_linter <- function(indent = 2L, hanging_indent_style = c("tidy", "al
223223
# + if there is no token following ( on the same line, a block indent is required until )
224224
# - binary operators where the second arguments starts on a new line
225225

226-
indent_levels <- rex::re_matches(
226+
indent_levels <- re_matches(
227227
source_expression$file_lines,
228-
rex::rex(start, any_spaces), locations = TRUE
228+
rex(start, any_spaces), locations = TRUE
229229
)[, "end"]
230230
expected_indent_levels <- integer(length(indent_levels))
231231
is_hanging <- logical(length(indent_levels))

R/lint.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ lint <- function(filename, linters = NULL, ..., cache = FALSE, parse_settings =
3939
stop("'cache' is no longer available as a positional argument; please supply 'cache' as a named argument instead.")
4040
}
4141

42-
needs_tempfile <- missing(filename) || rex::re_matches(filename, rex::rex(newline))
42+
needs_tempfile <- missing(filename) || re_matches(filename, rex(newline))
4343
inline_data <- !is.null(text) || needs_tempfile
4444
lines <- get_lines(filename, text)
4545

@@ -125,7 +125,7 @@ lint <- function(filename, linters = NULL, ..., cache = FALSE, parse_settings =
125125
lint_dir <- function(path = ".", ...,
126126
relative_path = TRUE,
127127
exclusions = list("renv", "packrat"),
128-
pattern = rex::rex(".", one_of("Rr"), or("", "html", "md", "nw", "rst", "tex", "txt"), end),
128+
pattern = rex(".", one_of("Rr"), or("", "html", "md", "nw", "rst", "tex", "txt"), end),
129129
parse_settings = TRUE) {
130130
if (has_positional_logical(list(...))) {
131131
stop(
@@ -738,7 +738,7 @@ maybe_append_error_lint <- function(lints, error, lint_cache, filename) {
738738
get_lines <- function(filename, text) {
739739
if (!is.null(text)) {
740740
strsplit(paste(text, collapse = "\n"), "\n", fixed = TRUE)[[1L]]
741-
} else if (rex::re_matches(filename, rex::rex(newline))) {
741+
} else if (re_matches(filename, rex(newline))) {
742742
strsplit(gsub("\n$", "", filename), "\n", fixed = TRUE)[[1L]]
743743
} else {
744744
read_lines(filename)

R/lintr-package.R

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@
88
"_PACKAGE"
99

1010
## lintr namespace: start
11+
#' @importFrom cyclocomp cyclocomp
1112
#' @importFrom glue glue glue_collapse
1213
#' @importFrom rex rex regex re_matches re_substitutes character_class
1314
#' @importFrom stats na.omit
14-
#' @importFrom utils capture.output head getParseData relist
15+
#' @importFrom utils capture.output getParseData globalVariables head relist tail
1516
#' @importFrom xml2 xml_attr xml_find_all xml_find_chr xml_find_num xml_find_first xml_name xml_text as_list
16-
#' @importFrom cyclocomp cyclocomp
17-
#' @importFrom utils tail
1817
#' @rawNamespace
1918
#' if (getRversion() >= "4.0.0") {
2019
#' importFrom(tools, R_user_dir)

R/methods.R

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ markdown <- function(x, info, ...) {
4545
as.character(x$line_number), ":",
4646
as.character(x$column_number), ":", "]",
4747
"(",
48-
paste(
49-
sep = "/",
48+
file.path(
5049
"https://github.com",
5150
info$user,
5251
info$repo,
@@ -122,7 +121,7 @@ trim_output <- function(x, max = 65535L) {
122121
# otherwise trim x to the max, then search for the lint starts
123122
x <- substr(x, 1L, max)
124123

125-
re <- rex::rex(
124+
re <- rex(
126125
"[", except_some_of(":"), ":", numbers, ":", numbers, ":", "]",
127126
"(", except_some_of(")"), ")",
128127
space,
@@ -134,7 +133,7 @@ trim_output <- function(x, max = 65535L) {
134133
except_some_of("\r\n"), newline
135134
)
136135

137-
lint_starts <- rex::re_matches(x, re, global = TRUE, locations = TRUE)[[1L]]
136+
lint_starts <- re_matches(x, re, global = TRUE, locations = TRUE)[[1L]]
138137

139138
# if at least one lint ends before the cutoff, cutoff there, else just use
140139
# the cutoff

R/object_usage_linter.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ object_usage_linter <- function(interpret_glue = TRUE, skip_with = TRUE) {
5959

6060
pkg_name <- pkg_name(find_package(dirname(source_expression$filename)))
6161

62-
declared_globals <- try_silently(utils::globalVariables(package = pkg_name %||% globalenv()))
62+
declared_globals <- try_silently(globalVariables(package = pkg_name %||% globalenv()))
6363

6464
xml <- source_expression$full_xml_parsed_content
6565

@@ -93,7 +93,7 @@ object_usage_linter <- function(interpret_glue = TRUE, skip_with = TRUE) {
9393

9494
# TODO handle assignment functions properly
9595
# e.g. `not_existing<-`(a, b)
96-
res$name <- rex::re_substitutes(res$name, rex::rex("<-"), "")
96+
res$name <- re_substitutes(res$name, rex("<-"), "")
9797

9898
lintable_symbols <- xml_find_all(fun_assignment, xpath_culprit_symbol)
9999

R/paste_linter.R

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#' `paste()` with `sep = ""` is not linted.
2222
#' @param allow_to_string Logical, default `FALSE`. If `TRUE`, usage of
2323
#' `paste()` and `paste0()` with `collapse = ", "` is not linted.
24+
#' @param allow_file_path Logical, default `FALSE`. If `TRUE`, usage of
25+
#' `paste()` and `paste0()` to construct file paths is not linted.
2426
#'
2527
#' @examples
2628
#' # will produce lints
@@ -44,6 +46,11 @@
4446
#' linters = paste_linter()
4547
#' )
4648
#'
49+
#' lint(
50+
#' text = 'paste0(dir, "/", file)',
51+
#' linters = paste_linter()
52+
#' )
53+
#'
4754
#' # okay
4855
#' lint(
4956
#' text = 'paste0("a", "b")',
@@ -75,9 +82,14 @@
7582
#' linters = paste_linter()
7683
#' )
7784
#'
85+
#' lint(
86+
#' text = 'paste0(year, "/", month, "/", day)',
87+
#' linters = paste_linter(allow_file_path = TRUE)
88+
#' )
89+
#'
7890
#' @seealso [linters] for a complete list of linters available in lintr.
7991
#' @export
80-
paste_linter <- function(allow_empty_sep = FALSE, allow_to_string = FALSE) {
92+
paste_linter <- function(allow_empty_sep = FALSE, allow_to_string = FALSE, allow_file_path = FALSE) {
8193
sep_xpath <- "
8294
//SYMBOL_FUNCTION_CALL[text() = 'paste']
8395
/parent::expr
@@ -111,6 +123,70 @@ paste_linter <- function(allow_empty_sep = FALSE, allow_to_string = FALSE) {
111123
/parent::expr
112124
"
113125

126+
slash_str <- sprintf("STR_CONST[%s]", xp_text_in_table(c("'/'", '"/"')))
127+
str_not_start_with_slash <-
128+
"STR_CONST[not(substring(text(), 2, 1) = '/')]"
129+
str_not_end_with_slash <-
130+
"STR_CONST[not(substring(text(), string-length(text()) - 1, 1) = '/')]"
131+
non_str <- "SYMBOL or expr"
132+
133+
# Type I: paste(..., sep = "/")
134+
paste_file_path_xpath <- glue("
135+
//SYMBOL_FUNCTION_CALL[text() = 'paste']
136+
/parent::expr
137+
/parent::expr[
138+
SYMBOL_SUB[text() = 'sep']/following-sibling::expr[1][{slash_str}]
139+
and not(SYMBOL_SUB[text() = 'collapse'])
140+
]
141+
")
142+
143+
# Type II: paste0(x, "/", y, "/", z)
144+
paste0_file_path_xpath <- xp_strip_comments(glue("
145+
//SYMBOL_FUNCTION_CALL[text() = 'paste0']
146+
/parent::expr
147+
/parent::expr[
148+
(: exclude paste0(x) :)
149+
count(expr) > 2
150+
(: An expression matching _any_ of these conditions is _not_ a file path :)
151+
and not(
152+
(: Any numeric input :)
153+
expr/NUM_CONST
154+
(: A call using collapse= :)
155+
or SYMBOL_SUB[text() = 'collapse']
156+
(: First input is '/', meaning file.path() would need to start with '' :)
157+
or expr[2][{slash_str}]
158+
(: Last input is '/', meaning file.path() would need to end with '' :)
159+
or expr[last()][{slash_str}]
160+
(: String starting or ending with multiple / :)
161+
(: TODO(#2075): run this logic on the actual R string :)
162+
or expr/STR_CONST[
163+
(: NB: this is (text, initial_index, n_characters) :)
164+
substring(text(), 2, 2) = '//'
165+
or substring(text(), string-length(text()) - 2, 2) = '//'
166+
]
167+
(: Consecutive non-strings like paste0(x, y) :)
168+
or expr[({non_str}) and following-sibling::expr[1][{non_str}]]
169+
(: A string not ending with /, followed by non-string or string not starting with / :)
170+
or expr[
171+
{str_not_end_with_slash}
172+
and following-sibling::expr[1][
173+
{non_str}
174+
or {str_not_start_with_slash}
175+
]
176+
]
177+
(: A string not starting with /, preceded by a non-string :)
178+
(: NB: consecutive strings is covered by the previous condition :)
179+
or expr[
180+
{str_not_start_with_slash}
181+
and preceding-sibling::expr[1][{non_str}]
182+
]
183+
)
184+
]
185+
"))
186+
187+
empty_paste_note <-
188+
'Note that paste() converts empty inputs to "", whereas file.path() leaves it empty.'
189+
114190
Linter(function(source_expression) {
115191
if (!is_lint_level(source_expression, "expression")) {
116192
return(list())
@@ -170,6 +246,32 @@ paste_linter <- function(allow_empty_sep = FALSE, allow_to_string = FALSE) {
170246
type = "warning"
171247
)
172248

249+
if (!allow_file_path) {
250+
paste_file_path_expr <- xml_find_all(xml, paste_file_path_xpath)
251+
optional_lints <- c(optional_lints, xml_nodes_to_lints(
252+
paste_file_path_expr,
253+
source_expression = source_expression,
254+
lint_message = paste(
255+
'Construct file paths with file.path(...) instead of paste(..., sep = "/").',
256+
'If you are using paste(sep = "/") to construct a date,',
257+
"consider using format() or lubridate helpers instead.",
258+
empty_paste_note
259+
),
260+
type = "warning"
261+
))
262+
263+
paste0_file_path_expr <- xml_find_all(xml, paste0_file_path_xpath)
264+
optional_lints <- c(optional_lints, xml_nodes_to_lints(
265+
paste0_file_path_expr,
266+
source_expression = source_expression,
267+
lint_message = paste(
268+
'Construct file paths with file.path(...) instead of paste0(x, "/", y, "/", z).',
269+
empty_paste_note
270+
),
271+
type = "warning"
272+
))
273+
}
274+
173275
c(optional_lints, paste0_sep_lints, paste_strrep_lints)
174276
})
175277
}

0 commit comments

Comments
 (0)