Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

keyword_quote_linter for unnecessary quoting #2030

Merged
merged 28 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bf390de
keyword_quote_linter for unnecessary quoting
MichaelChirico Aug 1, 2023
9b528d0
trim_some
MichaelChirico Aug 1, 2023
93e173e
import glue
MichaelChirico Aug 1, 2023
d118360
glue::glue -> glue
MichaelChirico Aug 1, 2023
29f5e8c
other glue functions
MichaelChirico Aug 1, 2023
7d49598
xml2:: replacement
MichaelChirico Aug 1, 2023
c7968de
more namespace importing
MichaelChirico Aug 1, 2023
67b5289
add examples
MichaelChirico Aug 2, 2023
92159c9
Merge branch 'main' into keyword_quote
MichaelChirico Aug 7, 2023
2c640a2
restore
MichaelChirico Aug 7, 2023
4f26866
line width
MichaelChirico Aug 7, 2023
2ca0de1
syntax in example
MichaelChirico Aug 7, 2023
8153028
remove explicit returns
MichaelChirico Aug 7, 2023
423a37e
use make.names()
MichaelChirico Aug 7, 2023
a25776e
reserved_words check not needed
MichaelChirico Aug 7, 2023
74866e9
unused constant
MichaelChirico Aug 7, 2023
18ab58e
split out two types of lints for extraction,assignment
MichaelChirico Aug 7, 2023
c76cf6c
small tweak
MichaelChirico Aug 7, 2023
b22457b
NEWS entry
MichaelChirico Aug 7, 2023
75afaf8
use xml_children, add tests, use shared names in tests
MichaelChirico Aug 8, 2023
ece4a4f
Merge branch 'main' into keyword_quote
MichaelChirico Aug 8, 2023
5e4e743
double quote
MichaelChirico Aug 8, 2023
fece979
xml_child
MichaelChirico Aug 8, 2023
b6bf4d1
Merge branch 'main' into keyword_quote
MichaelChirico Aug 8, 2023
aef3613
restore xml_children with comment
MichaelChirico Aug 8, 2023
39528e6
clarify comment
MichaelChirico Aug 8, 2023
522b02c
correct note about xml_child()
MichaelChirico Aug 8, 2023
df1114c
fix NEWS snippet
MichaelChirico Aug 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Collate:
'inner_combine_linter.R'
'is_lint_level.R'
'is_numeric_linter.R'
'keyword_quote_linter.R'
'lengths_linter.R'
'library_call_linter.R'
'line_length_linter.R'
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export(infix_spaces_linter)
export(inner_combine_linter)
export(is_lint_level)
export(is_numeric_linter)
export(keyword_quote_linter)
export(lengths_linter)
export(library_call_linter)
export(line_length_linter)
Expand Down Expand Up @@ -162,4 +163,5 @@ importFrom(xml2,xml_find_all)
importFrom(xml2,xml_find_chr)
importFrom(xml2,xml_find_first)
importFrom(xml2,xml_find_num)
importFrom(xml2,xml_name)
importFrom(xml2,xml_text)
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
+ `unreachable_code_linter()`
+ `yoda_test_linter()`

### New linters

* `keyword_quote_linter()` for finding unnecessary or discouraged quoting of symbols in assignment, function arguments, or extraction (part of #884, @MichaelChirico). Quoting is unnecessary when the target is a valid R name, e.g. `c("a" = 1)` can be `c(a = 1)`. The same goes to assignment (`"a" <- 1`) and extraction (`x$"a"`). Where quoting is necessary, the linter encourages doing so with backticks (e.g. `` x$`a b` `` instead of `x$"a"`).
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved

## Changes to defaults

* `assignment_linter()` lints the {magrittr} assignment pipe `%<>%` (#2008, @MichaelChirico). This can be deactivated by setting the new argument `allow_pipe_assign` to `TRUE`.
Expand Down
162 changes: 162 additions & 0 deletions R/keyword_quote_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#' Block unnecessary quoting in calls
#'
#' Any valid symbol can be used as a keyword argument to an R function call.
#' Sometimes, it is necessary to quote (or backtick) an argument that is
#' not an otherwise valid symbol (e.g. creating a vector whose names have
#' spaces); besides this edge case, quoting should not be done.
#'
#' The most common source of violation for this is creating named vectors,
#' lists, or data.frame-alikes, but it can be observed in other calls as well.
#'
#' Similar reasoning applies to extractions with `$` or `@`.
#'
#' @examples
#' # will produce lints
#' lint(
#' text = 'data.frame("a" = 1)',,
#' linters = keyword_quote_linter()
#' )
#'
#' lint(
#' text = "data.frame(`a` = 1)",
#' linters = keyword_quote_linter()
#' )
#'
#' lint(
#' text = 'my_list$"key"',
#' linters = keyword_quote_linter()
#' )
#'
#' lint(
#' text = 's4obj@"key"',
#' linters = keyword_quote_linter()
#' )
#'
#' # okay
#' lint(
#' text = "data.frame(`a b` = 1)",
#' linters = keyword_quote_linter()
#' )
#'
#' lint(
#' text = 'my_list$`a b`',
#' linters = keyword_quote_linter()
#' )
#'
#' @evalRd rd_tags("keyword_quote_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
# TODO(michaelchirico): offer a stricter version of this that
# requires backticks to be used for non-syntactic names (i.e., not quotes).
# Here are the relevant xpaths:
# //expr[expr[SYMBOL_FUNCTION_CALL]]/SYMBOL_SUB[starts-with(text(), '`')]
# //expr[expr[SYMBOL_FUNCTION_CALL]]/STR_CONST[{is_quoted(text())}]
keyword_quote_linter <- function() {
# NB: xml2 uses xpath 1.0 which doesn't support matches() for regex, so we
# have to jump out of xpath to complete this lint.
# It's also a bit tough to get the escaping through R and then xpath to
# work as intended, hence the rather verbose declaration here.
quote_cond <- xp_or(
"starts-with(text(), '\"')",
"starts-with(text(), '`')",
'starts-with(text(), "\'")'
)
# SYMBOL_SUB for backticks, STR_CONST for quoted names
call_arg_xpath <- glue("
//SYMBOL_FUNCTION_CALL
/parent::expr
/parent::expr
/*[(self::SYMBOL_SUB or self::STR_CONST) and {quote_cond}]
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
")

# also exclude $ or @, which are handled below
assignment_xpath <- "
(//EQ_ASSIGN | //LEFT_ASSIGN[text() != ':='])
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
/preceding-sibling::expr[
not(OP-DOLLAR or OP-AT)
and (STR_CONST or SYMBOL[starts-with(text(), '`')])
]
"

extraction_xpath <- "
(//OP-DOLLAR | //OP-AT)/following-sibling::STR_CONST
| //OP-DOLLAR/following-sibling::SYMBOL[starts-with(text(), '`')]
| //OP-AT/following-sibling::SLOT[starts-with(text(), '`')]
"

no_quote_msg <- "Use backticks to create non-syntactic names, not quotes."
clarification <- "i.e., if the name is not a valid R symbol (see ?make.names)."

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

xml <- source_expression$xml_parsed_content

call_arg_expr <- xml_find_all(xml, call_arg_xpath)

invalid_call_quoting <- is_valid_r_name(get_r_string(call_arg_expr))

call_arg_lints <- xml_nodes_to_lints(
call_arg_expr[invalid_call_quoting],
source_expression = source_expression,
lint_message = paste("Only quote named arguments to functions if necessary,", clarification),
type = "warning"
)

assignment_expr <- xml_find_all(xml, assignment_xpath)

invalid_assignment_quoting <- is_valid_r_name(get_r_string(assignment_expr))
assignment_to_string <- xml_name(xml_find_first(assignment_expr, "*")) == "STR_CONST"
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved

string_assignment_lints <- xml_nodes_to_lints(
assignment_expr[assignment_to_string & !invalid_assignment_quoting],
source_expression = source_expression,
lint_message = no_quote_msg,
type = "warning"
)

assignment_lints <- xml_nodes_to_lints(
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
assignment_expr[invalid_assignment_quoting],
source_expression = source_expression,
lint_message = paste("Only quote targets of assignment if necessary,", clarification),
type = "warning"
)

extraction_expr <- xml_find_all(xml, extraction_xpath)

invalid_extraction_quoting <- is_valid_r_name(get_r_string(extraction_expr))
extraction_of_string <- xml_name(extraction_expr) == "STR_CONST"

string_extraction_lints <- xml_nodes_to_lints(
extraction_expr[extraction_of_string & !invalid_extraction_quoting],
source_expression = source_expression,
lint_message = no_quote_msg,
type = "warning"
)

extraction_expr <- extraction_expr[invalid_extraction_quoting]
extractor <- xml_find_chr(extraction_expr, "string(preceding-sibling::*[1])")
gen_extractor <- ifelse(extractor == "$", "[[", "slot()")

extraction_lints <- xml_nodes_to_lints(
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
extraction_expr,
source_expression = source_expression,
lint_message = paste(
"Only quote targets of extraction with", extractor, "if necessary,", clarification,
"Use backticks to create non-syntactic names, or use", gen_extractor, "to extract by string."
),
type = "warning"
)

c(call_arg_lints, string_assignment_lints, assignment_lints, string_extraction_lints, extraction_lints)
})
}

#' Check if a string could be assigned as an R variable.
#'
#' See [make.names()] for the description of syntactically valid names in R.
#'
#' @noRd
is_valid_r_name <- function(x) make.names(x) == x
2 changes: 1 addition & 1 deletion R/lintr-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#' @importFrom rex rex regex re_matches re_substitutes character_class
#' @importFrom stats na.omit
#' @importFrom utils capture.output head getParseData relist
#' @importFrom xml2 xml_attr xml_find_all xml_find_chr xml_find_num xml_find_first xml_text as_list
#' @importFrom xml2 xml_attr xml_find_all xml_find_chr xml_find_num xml_find_first xml_name xml_text as_list
#' @importFrom cyclocomp cyclocomp
#' @importFrom utils tail
#' @rawNamespace
Expand Down
1 change: 1 addition & 0 deletions inst/lintr/linters.csv
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ indentation_linter,style readability default configurable
infix_spaces_linter,style readability default configurable
inner_combine_linter,efficiency consistency readability
is_numeric_linter,readability best_practices consistency
keyword_quote_linter,readability consistency style
lengths_linter,efficiency readability best_practices
library_call_linter,style best_practices readability
line_length_linter,style readability default configurable
Expand Down
1 change: 1 addition & 0 deletions man/consistency_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions man/keyword_quote_linter.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions man/linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/readability_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/style_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading