Skip to content

Commit

Permalink
Merge branch 'main' into object-usage-lambda
Browse files Browse the repository at this point in the history
  • Loading branch information
IndrajeetPatil authored Apr 2, 2023
2 parents f6e3916 + 01500d8 commit 3caa643
Show file tree
Hide file tree
Showing 32 changed files with 610 additions and 174 deletions.
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Collate:
'lintr-package.R'
'literal_coercion_linter.R'
'make_linter_from_regex.R'
'matrix_apply_linter.R'
'methods.R'
'missing_argument_linter.R'
'missing_package_linter.R'
Expand All @@ -139,6 +140,7 @@ Collate:
'path_utils.R'
'pipe_call_linter.R'
'pipe_continuation_linter.R'
'quotes_linter.R'
'redundant_equals_linter.R'
'redundant_ifelse_linter.R'
'regex_subset_linter.R'
Expand All @@ -147,7 +149,6 @@ Collate:
'seq_linter.R'
'settings.R'
'settings_utils.R'
'single_quotes_linter.R'
'sort_linter.R'
'spaces_inside_linter.R'
'spaces_left_parentheses_linter.R'
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export(lint_package)
export(linters_with_defaults)
export(linters_with_tags)
export(literal_coercion_linter)
export(matrix_apply_linter)
export(missing_argument_linter)
export(missing_package_linter)
export(modify_defaults)
Expand All @@ -103,6 +104,7 @@ export(paren_brace_linter)
export(paste_linter)
export(pipe_call_linter)
export(pipe_continuation_linter)
export(quotes_linter)
export(redundant_equals_linter)
export(redundant_ifelse_linter)
export(regex_subset_linter)
Expand Down
25 changes: 19 additions & 6 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# lintr (development version)

## Deprecations

* `single_quotes_linter()` is deprecated in favor of the more generalizable `quotes_linter()` (#1729, @MichaelChirico).
* `unneeded_concatentation_linter()` is deprecated in favor of `unnecessary_concatenation_linter()` for naming consistency (#1707, @IndrajeetPatil).

## Bug fixes

* `fixed_regex_linter()` is more robust to errors stemming from unrecognized escapes (#1545, #1845, @IndrajeetPatil).
Expand All @@ -9,7 +14,9 @@

* `assignment_linter()` no longer lints assignments in braces that include comments when `allow_trailing = FALSE` (#1701, @ashbaldry)

* `object_usage_linter()` no longer silently ignores usage warnings that don't contain a quoted name (#1714, @AshesITR)
* `object_usage_linter()`
+ No longer silently ignores usage warnings that don't contain a quoted name (#1714, @AshesITR)
+ No longer fails on code with comments inside a multi-line call to `glue::glue()` (#1919, @MichaelChirico)

* `namespace_linter()` correctly recognizes backticked operators to be exported from respective namespaces (like `` rlang::`%||%` ``) (#1752, @IndrajeetPatil)

Expand Down Expand Up @@ -45,14 +52,14 @@
the style guide on handling this case awaits clarification: https://github.com/tidyverse/style/issues/191.
(#1346, @MichaelChirico)

* The new `indentation_linter()` is part of the default linters. See "New linters" for more details.

* For naming consistency, `unneeded_concatenation_linter()` has been deprecated in favor of
`unnecessary_concatenation_linter()` (#1797, @IndrajeetPatil).

* `undesirable_function_linter()` and `undesirable_operator_linter()` now produce an error
if empty vector of undesirable functions or operators is provided (#1867, @IndrajeetPatil).

* New linters which are also included as defaults (see "New linters" for more details):
+ `indentation_linter()`
+ `quotes_linter()`
+ `unnecessary_concatenation_linter()`

## New and improved features

* New `get_r_string()` helper to get the R-equivalent value of a string, especially useful for R-4-style raw strings.
Expand Down Expand Up @@ -101,6 +108,8 @@

### New linters

* `matrix_apply_linter()` recommends use of dedicated `rowSums()`, `colSums()`, `colMeans()`, `rowMeans()` over `apply(., MARGIN, sum)` or `apply(., MARGIN, mean)`. The recommended alternative is much more efficient and more readable (#1869, @Bisaloo).

* `unnecessary_lambda_linter()`: detect unnecessary lambdas (anonymous functions), e.g.
`lapply(x, function(xi) sum(xi))` can be `lapply(x, sum)` and `purrr::map(x, ~quantile(.x, 0.75, na.rm = TRUE))`
can be `purrr::map(x, quantile, 0.75, na.rm = TRUE)`. Naming `probs = 0.75` can further improve readability (#1531, #1866, @MichaelChirico, @Bisaloo).
Expand Down Expand Up @@ -132,6 +141,10 @@

* `implicit_assignment_linter()` for checking implicit assignments in function calls (@IndrajeetPatil and @AshesITR, #1777).

* `quotes_linter()` is a generalized version of (now deprecated) `single_quotes_linter()`. It accepts an argument `delimiter` to specify whether `"` or `'` should be the accepted method for delimiting character literals. The default, `"`, reflects the Tidyverse style guide recommendation and matches the behavior of `single_quotes_linter()`.

* `unnecessary_concatenation_linter()` is simply `unneeded_concatenation_linter()`, renamed.

## Notes

* {lintr} now depends on R version 3.5.0, in line with the tidyverse policy for R version compatibility.
Expand Down
13 changes: 13 additions & 0 deletions R/lintr-deprecated.R
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,16 @@ find_column_fun <- function(content, newline_locs) {
line_number - newline_locs[matched_line_number]
}
}

#' Single quotes linter
#' @rdname lintr-deprecated
#' @export
single_quotes_linter <- function() {
lintr_deprecated(
old = "single_quotes_linter",
new = "quotes_linter",
version = "3.1.0",
type = "Linter"
)
quotes_linter()
}
153 changes: 153 additions & 0 deletions R/matrix_apply_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#' Require usage of `colSums(x)` or `rowSums(x)` over `apply(x, ., sum)`
#'
#' [colSums()] and [rowSums()] are clearer and more performant alternatives to
#' `apply(x, 2, sum)` and `apply(x, 1, sum)` respectively in the case of 2D
#' arrays, or matrices
#'
#' @examples
#' # will produce lints
#' lint(
#' text = "apply(x, 1, sum)",
#' linters = matrix_apply_linter()
#' )
#'
#' lint(
#' text = "apply(x, 2, sum)",
#' linters = matrix_apply_linter()
#' )
#'
#' lint(
#' text = "apply(x, 2, sum, na.rm = TRUE)",
#' linters = matrix_apply_linter()
#' )
#'
#' lint(
#' text = "apply(x, 2:4, sum)",
#' linters = matrix_apply_linter()
#' )
#'
#' @evalRd rd_tags("matrix_apply_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
matrix_apply_linter <- function() {

# mean() and sum() have very different signatures so we treat them separately.
# sum() takes values to sum over via ..., has just one extra argument and is not a generic
# mean() is a generic, takes values to average via a single argument, and can have extra arguments
#
# Currently supported values for MARGIN: scalar numeric and vector of contiguous values created by : (OP-COLON)
sums_xpath <- "
//SYMBOL_FUNCTION_CALL[text() = 'apply']
/parent::expr
/following-sibling::expr[
NUM_CONST or OP-COLON/preceding-sibling::expr[NUM_CONST]/following-sibling::expr[NUM_CONST]
and (position() = 2)
]
/following-sibling::expr[
SYMBOL[text() = 'sum']
and (position() = 1)
]
/parent::expr
"

# Since mean() is a generic, we make sure that we only lint cases with arguments
# supported by colMeans() and rowMeans(), i.e., na.rm
means_xpath <- "
//SYMBOL_FUNCTION_CALL[text() = 'apply']
/parent::expr
/following-sibling::expr[
NUM_CONST or OP-COLON/preceding-sibling::expr[NUM_CONST]/following-sibling::expr[NUM_CONST]
and (position() = 2)
]
/following-sibling::expr[
SYMBOL[text() = 'mean']
and (position() = 1)
]
/parent::expr[
count(expr) = 4
or (count(expr) = 5 and SYMBOL_SUB[text() = 'na.rm'])
]
"

xpath <- glue::glue("{sums_xpath} | {means_xpath}")

# This doesn't handle the case when MARGIN and FUN are named and in a different position
# but this should be relatively rate
var_xpath <- "expr[position() = 2]"
margin_xpath <- "expr[position() = 3]"
fun_xpath <- "expr[position() = 4]"

Linter(function(source_expression) {
if (!is_lint_level(source_expression, "expression")) {
return(list())
}
xml <- source_expression$xml_parsed_content

bad_expr <- xml2::xml_find_all(xml, xpath)

var <- xml2::xml_text(xml2::xml_find_all(bad_expr, var_xpath))

fun <- xml2::xml_text(xml2::xml_find_all(bad_expr, fun_xpath))
fun <- tools::toTitleCase(fun)

margin <- xml2::xml_find_all(bad_expr, margin_xpath)

narm_val <- xml2::xml_text(
xml2::xml_find_first(bad_expr, "SYMBOL_SUB[text() = 'na.rm']/following-sibling::expr")
)

recos <- Map(craft_colsums_rowsums_msg, var, margin, fun, narm_val)

xml_nodes_to_lints(
bad_expr,
source_expression = source_expression,
lint_message = sprintf("Use %1$s rather than %2$s", recos, get_r_string(bad_expr)),
type = "warning"
)
})
}

craft_colsums_rowsums_msg <- function(var, margin, fun, narm_val) {

if (is.na(xml2::xml_find_first(margin, "OP-COLON"))) {
l1 <- xml2::xml_text(margin)
l2 <- NULL
} else {
l1 <- xml2::xml_text(xml2::xml_find_first(margin, "expr[1]"))
l2 <- xml2::xml_text(xml2::xml_find_first(margin, "expr[2]"))
}

# See #1764 for details about various cases. In short:
# - If apply(., 1:l2, sum) -> rowSums(., dims = l2)
# - If apply(., l1:l2, sum) -> rowSums(colSums(., dims = l1 - 1), dims = l2 - l1 + 1)
# - This last case can be simplified to a simple colSums() call if l2 = length(dim(.))
# - dims argument can be dropped if equals to 1. This notably is the case for matrices
if (is.null(l2)) {
l2 <- l1
}

# We don't want warnings when converted as NAs
l1 <- suppressWarnings(as.integer(re_substitutes(l1, "L$", "")))
l2 <- suppressWarnings(as.integer(re_substitutes(l2, "L$", "")))

if (!is.na(narm_val)) {
narm <- glue::glue(", na.rm = {narm_val}")
} else {
narm <- ""
}

if (identical(l1, 1L)) {
reco <- glue::glue("row{fun}s({var}{narm}, dims = {l2})")
} else {
reco <- glue::glue(
"row{fun}s(col{fun}s({var}{narm}, dims = {l1 - 1}), dims = {l2 - l1 + 1})",
" or ",
"col{fun}s({var}{narm}, dims = {l1 - 1}) if {var} has {l2} dimensions"
)
}

# It's easier to remove this after the fact, rather than having never ending if/elses
reco <- gsub(", dims = 1", "", reco, fixed = TRUE)

return(reco)
}
4 changes: 2 additions & 2 deletions R/object_usage_linter.R
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ extract_glued_symbols <- function(expr) {
stop("Unexpected failure to parse glue call, please report: ", conditionMessage(cond)) # nocov
}
glued_symbols <- new.env(parent = emptyenv())
for (call_text in xml2::xml_text(glue_calls)) {
for (glue_call in glue_calls) {
# TODO(michaelchirico): consider dropping tryCatch() here if we're more confident in our logic
parsed_call <- tryCatch(str2lang(call_text), error = unexpected_error, warning = unexpected_error)
parsed_call <- tryCatch(xml2lang(glue_call), error = unexpected_error, warning = unexpected_error)
parsed_call[[".envir"]] <- glued_symbols
parsed_call[[".transformer"]] <- symbol_extractor
# #1459: syntax errors in glue'd code are ignored with warning, rather than crashing lint
Expand Down
91 changes: 91 additions & 0 deletions R/quotes_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#' Character string quote linter
#'
#' Check that the desired quote delimiter is used for string constants.
#'
#' @param delimiter Which quote delimiter to accept. Defaults to the tidyverse
#' default of `"` (double-quoted strings).
#'
#' @examples
#' # will produce lints
#' lint(
#' text = "c('a', 'b')",
#' linters = quotes_linter()
#' )
#'
#' # okay
#' lint(
#' text = 'c("a", "b")',
#' linters = quotes_linter()
#' )
#'
#' code_lines <- "paste0(x, '\"this is fine\"')"
#' writeLines(code_lines)
#' lint(
#' text = code_lines,
#' linters = quotes_linter()
#' )
#'
#' # okay
#' lint(
#' text = "c('a', 'b')",
#' linters = quotes_linter(delimiter = "'")
#' )
#'
#' @evalRd rd_tags("quotes_linter")
#' @seealso
#' - [linters] for a complete list of linters available in lintr.
#' - <https://style.tidyverse.org/syntax.html#character-vectors>
#' @export
quotes_linter <- function(delimiter = c('"', "'")) {
delimiter <- match.arg(delimiter)
if (delimiter == '"') {
quote_regex <- rex(
start,
zero_or_one(character_class("rR")),
single_quote,
any_non_double_quotes,
single_quote,
end
)
lint_message <- "Only use double-quotes." # nolint: object_usage_linter. An apparent codetools bug.
} else {
quote_regex <- rex(
start,
zero_or_one(character_class("rR")),
double_quote,
any_non_single_quotes,
double_quote,
end
)
lint_message <- "Only use single-quotes."
}

Linter(function(source_expression) {
if (!is_lint_level(source_expression, "file")) {
return(list())
}

content <- source_expression$full_parsed_content
str_idx <- which(content$token == "STR_CONST")
quote_matches <- which(re_matches(content[str_idx, "text"], quote_regex))

lapply(
quote_matches,
function(id) {
with(content[str_idx[id], ], {
line <- source_expression$file_lines[[line1]]
col2 <- if (line1 == line2) col2 else nchar(line)
Lint(
filename = source_expression$filename,
line_number = line1,
column_number = col1,
type = "style",
message = lint_message,
line = line,
ranges = list(c(col1, col2))
)
})
}
)
})
}
Loading

0 comments on commit 3caa643

Please sign in to comment.