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 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
|
#' Object name linter
#'
#' Check that object names conform to a naming style.
#' The default naming styles are "snake_case" and "symbols".
#'
#' Quotes (`` `"' ``) and specials (`%` and trailing `<-`) are not considered part of the object name.
#'
#' Note when used in a package, in order to ignore objects imported
#' from other namespaces, this linter will attempt [getNamespaceExports()]
#' whenever an `import(PKG)` or `importFrom(PKG, ...)` statement is found
#' in your NAMESPACE file. If [requireNamespace()] fails (e.g., the package
#' is not yet installed), the linter won't be able to ignore some usages
#' that would otherwise be allowed.
#'
#' Suppose, for example, you have `import(upstream)` in your NAMESPACE,
#' which makes available its exported S3 generic function
#' `a_really_quite_long_function_name` that you then extend in your package
#' by defining a corresponding method for your class `my_class`.
#' Then, if `upstream` is not installed when this linter runs, a lint
#' will be thrown on this object (even though you don't "own" its full name).
#'
#' The best way to get lintr to work correctly is to install the package so
#' that it's available in the session where this linter is running.
#'
#' @param styles A subset of
#' \Sexpr[stage=render, results=rd]{lintr:::regexes_rd}. A name should
#' match at least one of these styles. The `"symbols"` style refers to
#' names containing *only* non-alphanumeric characters; e.g., defining `%+%`
#' from ggplot2 or `%>%` from magrittr would not generate lint markers,
#' whereas `%m+%` from lubridate (containing both alphanumeric *and*
#' non-alphanumeric characters) would.
#'
#' @param regexes A (possibly named) character vector specifying a custom naming convention.
#' If named, the names will be used in the lint message. Otherwise, the regexes enclosed by `/` will be used in the
#' lint message.
#' Note that specifying `regexes` overrides the default `styles`. So if you want to combine `regexes` and `styles`,
#' both need to be explicitly specified.
#'
#' @examples
#' # will produce lints
#' lint(
#' text = "my_var <- 1L",
#' linters = object_name_linter(styles = "CamelCase")
#' )
#'
#' lint(
#' text = "xYz <- 1L",
#' linters = object_name_linter(styles = c("UPPERCASE", "lowercase"))
#' )
#'
#' lint(
#' text = "MyVar <- 1L",
#' linters = object_name_linter(styles = "dotted.case")
#' )
#'
#' lint(
#' text = "asd <- 1L",
#' linters = object_name_linter(regexes = c(my_style = "F$", "f$"))
#' )
#'
#' # okay
#' lint(
#' text = "my_var <- 1L",
#' linters = object_name_linter(styles = "snake_case")
#' )
#'
#' lint(
#' text = "xyz <- 1L",
#' linters = object_name_linter(styles = "lowercase")
#' )
#'
#' lint(
#' text = "my.var <- 1L; myvar <- 2L",
#' linters = object_name_linter(styles = c("dotted.case", "lowercase"))
#' )
#'
#' lint(
#' text = "asdf <- 1L; asdF <- 1L",
#' linters = object_name_linter(regexes = c(my_style = "F$", "f$"))
#' )
#'
#' @evalRd rd_tags("object_name_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
object_name_linter <- function(styles = c("snake_case", "symbols"), regexes = character()) {
if ((!missing(styles) || missing(regexes)) && length(styles) > 0L) {
# Allow `object_name_linter(NULL, "my_regex")`
styles <- match.arg(styles, names(style_regexes), several.ok = TRUE)
style_list <- style_regexes[styles]
} else {
style_list <- list()
}
if (length(regexes) > 0L) {
if (!is.character(regexes)) {
cli_abort("{.arg regexes} must be a {.cls character} vector.")
}
rx_names <- names2(regexes)
missing_name <- !nzchar(rx_names)
rx_names[missing_name] <- paste0("/", regexes[missing_name], "/") # auto-name regex "asd" -> /asd/
names(regexes) <- rx_names
style_list <- c(style_list, as.list(regexes))
}
if (length(style_list) == 0L) {
cli_abort("At least one style must be specified using {.arg styles} or {.arg regexes}.")
}
lint_message <- paste0(
"Variable and function name style should match ",
glue_collapse(unique(names(style_list)), sep = ", ", last = " or "), "."
)
Linter(linter_level = "file", function(source_expression) {
xml <- source_expression$full_xml_parsed_content
assignments <- xml_find_all(xml, object_name_xpath)
# Retrieve assigned name
nms <- strip_names(
xml_text(assignments)
)
# run namespace_imports at run-time, not "compile" time to allow package structure to change
pkg <- find_package(source_expression$filename)
generics <- c(
declared_s3_generics(xml),
imported_s3_generics(namespace_imports(pkg))$fun,
exported_s3_generics(pkg)$fun,
.base_s3_generics
)
generics <- unique(generics[nzchar(generics)])
style_matches <- lapply(style_list, function(style) {
check_style(nms, style, generics)
})
matches_a_style <- Reduce(`|`, style_matches)
xml_nodes_to_lints(
assignments[!matches_a_style],
source_expression,
lint_message = lint_message,
type = "style"
)
})
}
check_style <- function(nms, style, generics = character()) {
conforming <- re_matches_logical(nms, style)
# mark empty or NA names as conforming
conforming <- is.na(nms) | !nzchar(nms) | conforming
if (!all(conforming)) {
possible_s3 <- re_matches(
nms[!conforming],
rex(start, capture(name = "generic", or(generics)), ".", capture(name = "method", something), end)
)
if (!all(is.na(possible_s3))) {
has_generic <- possible_s3$generic %in% generics
# If they are not conforming, but are S3 methods then ignore them
conforming[!conforming][has_generic] <- TRUE
}
# exclude namespace hooks like .onLoad, .Last.lib, etc (#500) and ...
is_special <- is_special_function(nms[!conforming]) | nms[!conforming] == "..."
conforming[!conforming][is_special] <- TRUE
}
conforming
}
loweralnum <- rex(one_of(lower, digit))
upperalnum <- rex(one_of(upper, digit))
style_regexes <- list(
symbols = rex(start, zero_or_more(none_of(alnum)), end),
CamelCase = rex(start, maybe("."), upper, zero_or_more(alnum), end),
camelCase = rex(start, maybe("."), lower, zero_or_more(alnum), end),
snake_case = rex(start, maybe("."), some_of(lower, digit), any_of("_", lower, digit), end),
SNAKE_CASE = rex(start, maybe("."), some_of(upper, digit), any_of("_", upper, digit), end),
dotted.case = rex(start, maybe("."), one_or_more(loweralnum), zero_or_more(dot, one_or_more(loweralnum)), end),
lowercase = rex(start, maybe("."), one_or_more(loweralnum), end),
UPPERCASE = rex(start, maybe("."), one_or_more(upperalnum), end)
)
regexes_rd <- toString(paste0("\\sQuote{", names(style_regexes), "}"))
|