Skip to content

Commit fc76887

Browse files
BisalooIndrajeetPatilMichaelChiricoAshesITR
authored
Add sort_linter() (#1528)
* Add basic tests * Add sort_linter() * Add tags for sort_linter * Run devtools::document() * Add NEWS item * Start path with //OP-LEFT-BRACKET instead of //expr * Customize variable name in warning message * Run devtools::document() * Add examples * Fix symbol grabbing * Customize warning messages * Use double quotes * tidy formatting of XPath * use rex() * Use rex::rex() to avoid excessive escaping * Assign linter to var in tests * Remove style tag * Add test for multiple lints * Fix var detection pattern * Add [1] to expr * Revert "Add test for multiple lints" This reverts commit e385865. * Move some elements to factory env * Link to sort() in docs * Add example for edge case with single index * Run devtools::document() * Define x & y in example * Fix var name * Add failing test for multiple lints * Fix test for multiple sort_linter() * Fix message for multiple sort_linter() * Fix problems detected by lintr Co-authored-by: Indrajeet Patil <[email protected]> Co-authored-by: Michael Chirico <[email protected]> Co-authored-by: AshesITR <[email protected]>
1 parent 9bb8b63 commit fc76887

File tree

11 files changed

+263
-3
lines changed

11 files changed

+263
-3
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ Collate:
147147
'settings.R'
148148
'settings_utils.R'
149149
'single_quotes_linter.R'
150+
'sort_linter.R'
150151
'spaces_inside_linter.R'
151152
'spaces_left_parentheses_linter.R'
152153
'sprintf_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export(semicolon_linter)
108108
export(semicolon_terminator_linter)
109109
export(seq_linter)
110110
export(single_quotes_linter)
111+
export(sort_linter)
111112
export(spaces_inside_linter)
112113
export(spaces_left_parentheses_linter)
113114
export(sprintf_linter)

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@
159159

160160
## New and improved features
161161

162+
* New `sort_linter()` to detect `x[order(x)]` and recommend the faster and clearer alternative: `sort(x)` (#1528, @Bisaloo)
163+
162164
* `unreachable_code_linter()` ignores trailing comments if they match a closing nolint block (#1347, @AshesITR).
163165

164166
* New `function_argument_linter()` to enforce that arguments with defaults appear last in function declarations,

R/sort_linter.R

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#' Require usage of `sort()` over `.[order(.)]`
2+
#'
3+
#' [sort()] is the dedicated option to sort a list or vector. It is more legible
4+
#' and around twice as fast as `.[order(.)]`, with the gap in performance
5+
#' growing with the vector size.
6+
#'
7+
#' @examples
8+
#' # will produce lints
9+
#' lint(
10+
#' text = "x[order(x)]",
11+
#' linters = sort_linter()
12+
#' )
13+
#'
14+
#' lint(
15+
#' text = "x[order(x, decreasing = TRUE)]",
16+
#' linters = sort_linter()
17+
#' )
18+
#'
19+
#' # okay
20+
#' lint(
21+
#' text = "x[sample(order(x))]",
22+
#' linters = sort_linter()
23+
#' )
24+
#'
25+
#' lint(
26+
#' text = "y[order(x)]",
27+
#' linters = sort_linter()
28+
#' )
29+
#'
30+
#' # If you are sorting several objects based on the order of one of them, such
31+
#' # as:
32+
#' x <- sample(1:26)
33+
#' y <- letters
34+
#' newx <- x[order(x)]
35+
#' newy <- y[order(x)]
36+
#' # This will be flagged by the linter. However, in this very specific case,
37+
#' # it would be clearer and more efficient to run order() once and assign it
38+
#' # to an object, rather than mix and match order() and sort()
39+
#' index <- order(x)
40+
#' newx <- x[index]
41+
#' newy <- y[index]
42+
#'
43+
#' @evalRd rd_tags("sort_linter")
44+
#' @seealso [linters] for a complete list of linters available in lintr.
45+
#' @export
46+
sort_linter <- function() {
47+
xpath <- "
48+
//OP-LEFT-BRACKET
49+
/following-sibling::expr[1][
50+
expr[1][
51+
SYMBOL_FUNCTION_CALL[text() = 'order']
52+
and following-sibling::expr =
53+
parent::expr[1]
54+
/parent::expr[1]
55+
/expr[1]
56+
]
57+
]
58+
"
59+
60+
args_xpath <- ".//SYMBOL_SUB[text() = 'method' or
61+
text() = 'decreasing' or
62+
text() = 'na.last']"
63+
64+
arg_values_xpath <- glue::glue("{args_xpath}/following-sibling::expr[1]")
65+
66+
Linter(function(source_expression) {
67+
if (!is_lint_level(source_expression, "expression")) {
68+
return(list())
69+
}
70+
71+
xml <- source_expression$xml_parsed_content
72+
73+
bad_expr <- xml2::xml_find_all(xml, xpath)
74+
75+
var <- xml2::xml_text(
76+
xml2::xml_find_first(
77+
bad_expr,
78+
".//SYMBOL_FUNCTION_CALL[text() = 'order']/parent::expr[1]/following-sibling::expr[1]"
79+
)
80+
)
81+
82+
orig_call <- sprintf(
83+
"%1$s[%2$s]",
84+
var,
85+
get_r_string(bad_expr)
86+
)
87+
88+
# Reconstruct new argument call for each expression separately
89+
args <- vapply(bad_expr, function(e) {
90+
arg_names <- xml2::xml_text(xml2::xml_find_all(e, args_xpath))
91+
arg_values <- xml2::xml_text(
92+
xml2::xml_find_all(e, arg_values_xpath)
93+
)
94+
if (!"na.last" %in% arg_names) {
95+
arg_names <- c(arg_names, "na.last")
96+
arg_values <- c(arg_values, "TRUE")
97+
}
98+
toString(paste(arg_names, "=", arg_values))
99+
}, character(1L))
100+
101+
new_call <- sprintf(
102+
"sort(%1$s, %2$s)",
103+
var,
104+
args
105+
)
106+
107+
xml_nodes_to_lints(
108+
bad_expr,
109+
source_expression = source_expression,
110+
lint_message = paste0(new_call, " is better than ", orig_call, "."),
111+
type = "warning"
112+
)
113+
})
114+
}

inst/lintr/linters.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ semicolon_linter,style readability default configurable
6868
semicolon_terminator_linter,style readability deprecated configurable
6969
seq_linter,robustness efficiency consistency best_practices default
7070
single_quotes_linter,style consistency readability default
71+
sort_linter,readability best_practices efficiency
7172
spaces_inside_linter,style readability default
7273
spaces_left_parentheses_linter,style readability default
7374
sprintf_linter,correctness common_mistakes

man/best_practices_linters.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/efficiency_linters.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/linters.Rd

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/readability_linters.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/sort_linter.Rd

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)