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

Validate config files #2225

Merged
merged 39 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f5b0fe7
reorganize NEWS
MichaelChirico Oct 3, 2023
e96ac22
validate_config_file
MichaelChirico Oct 3, 2023
e943fd1
cross-reference in docs for clarity
MichaelChirico Oct 3, 2023
3986c31
bad branching
MichaelChirico Oct 3, 2023
4a8ab36
ugh
MichaelChirico Oct 3, 2023
71d8bd0
proper linking
MichaelChirico Oct 3, 2023
b6ed2f3
catch expected warning
MichaelChirico Oct 3, 2023
8427a65
attempt a more complete description of valid exclusions
MichaelChirico Oct 3, 2023
898fecf
grammar
MichaelChirico Oct 3, 2023
7360ae9
simplify validate_named_exclusion
MichaelChirico Oct 3, 2023
8bdfbe0
add regression tests
MichaelChirico Oct 3, 2023
67b0c76
delint
MichaelChirico Oct 3, 2023
e6ab0f9
allow vectors in exclusions
MichaelChirico Oct 4, 2023
4a9a762
document
MichaelChirico Oct 4, 2023
6cf4385
clearer warning
MichaelChirico Oct 4, 2023
bbcdb4c
correct warning in test
MichaelChirico Oct 4, 2023
602b720
reuse is_linter()
MichaelChirico Oct 4, 2023
ac7f67e
Merge branch 'main' into validate-config
MichaelChirico Oct 4, 2023
b96e738
add explicit tests of character vector exclusions
MichaelChirico Oct 4, 2023
10fbb39
Merge branch 'main' into validate-config
MichaelChirico Oct 4, 2023
ede3f3d
NEWS
MichaelChirico Oct 4, 2023
455228e
Merge branch 'main' into validate-config
MichaelChirico Oct 8, 2023
6a5c955
type-based validation
MichaelChirico Oct 8, 2023
e92af56
new tests
MichaelChirico Oct 8, 2023
0190baa
delint
MichaelChirico Oct 8, 2023
1b3fed2
Merge branch 'main' into validate-config
MichaelChirico Oct 8, 2023
a49ada5
Merge branch 'main' into validate-config
MichaelChirico Oct 9, 2023
36ce8c7
consistent naming
MichaelChirico Oct 9, 2023
da9278c
Merge remote-tracking branch 'origin/validate-config' into validate-c…
MichaelChirico Oct 9, 2023
04ecf8f
NEWS
MichaelChirico Oct 9, 2023
4a13f52
stale comment
MichaelChirico Oct 9, 2023
81515c7
simpler test
MichaelChirico Oct 9, 2023
c045a9d
attempt to clarify list naming, document Inf
MichaelChirico Oct 9, 2023
58e4afc
add a test of exclude Inf lines
MichaelChirico Oct 9, 2023
75b8af7
Merge branch 'main' into validate-config
MichaelChirico Oct 9, 2023
7757e5a
reword further
MichaelChirico Oct 9, 2023
7a5aa4d
delint
MichaelChirico Oct 9, 2023
4b2fb93
Merge branch 'validate-config' of github.com:r-lib/lintr into validat…
MichaelChirico Oct 9, 2023
74328be
Merge branch 'main' into validate-config
MichaelChirico Oct 9, 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 NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* `infix_spaces_linter()` distinguishes `<-`, `:=`, `<<-` and `->`, `->>`, i.e. `infix_spaces_linter(exclude_operators = "->")` will no longer exclude `->>` (#2115, @MichaelChirico). This change is breaking for users relying on manually-supplied `exclude_operators` containing `"<-"` to also exclude `:=` and `<<-`. The fix is to manually supply `":="` and `"<<-"` as well. We don't expect this change to affect many users, the fix is simple, and the new behavior is much more transparent, so we are including this breakage in a minor release.
* Removed `find_line()` and `find_column()` entries from `get_source_expressions()` expression-level objects. These have been marked deprecated since version 3.0.0. No users were found on GitHub.
* There is experimental support for writing config in plain R scripts (as opposed to DCF files; #1210, @MichaelChirico). The script is run in a new environment and variables matching settings (`?default_settings`) are copied over. In particular, this removes the need to write R code in a DCF-friendly way, and allows normal R syntax highlighting in the saved file. We may eventually deprecate the DCF approach in favor of this one; user feedback is welcome on strong preferences for either approach, or for a different approach like YAML. Generally you should be able to convert your existing `.lintr` file to an equivalent R config by replacing the `:` key-value separators with assignments (`<-`). By default, such a config is searched for in a file named '.lintr.R'. This is a mildly breaking change if you happened to be keeping a file '.lintr.R' around since that file is given precedence over '.lintr'.
+ We also validate config files up-front make it clearer when invalid configs are present (#2195, @MichaelChirico). There is a warning for "invalid" settings, i.e., settings not part of `?default_settings`. We think this is more likely to affect users declaring settings in R, since any variable defined in the config that's not a setting must be removed.
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved

## Bug fixes

Expand Down
16 changes: 11 additions & 5 deletions R/exclude.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@
#' "@details",
#' "Exclusions can be specified in three different ways.",
#' "",
#' "1. single line in the source file. default: `# nolint`, possibly followed by a listing of linters to exclude.",
#' "1. Single line in the source file. default: `# nolint`, possibly followed by a listing of linters to exclude.",
#' " If the listing is missing, all linters are excluded on that line. The default listing format is",
#' paste(
#' " `#",
#' "nolint: linter_name, linter2_name.`. There may not be anything between the colon and the line exclusion tag"
#' ),
#' " and the listing must be terminated with a full stop (`.`) for the linter list to be respected.",
#' "2. line range in the source file. default: `# nolint start`, `# nolint end`. `# nolint start` accepts linter",
#' "2. Line range in the source file. default: `# nolint start`, `# nolint end`. `# nolint start` accepts linter",
#' " lists in the same form as `# nolint`.",
#' "3. exclusions parameter, a named list of files with named lists of linters and lines to exclude them on, a named",
#' " list of the files and lines to exclude, or just the filenames if you want to exclude the entire file, or the",
#' " directory names if you want to exclude all files in a directory."
#' "3. Exclusions parameter, a list with named and/or unnamed entries. If present, the name is a path relative to",
#' " the config. Moreover, elements have the following characteristics:",
#' " 1. Unnamed elements specify filenames or directories.",
#' " 2. Named elements are numeric or a list. If present, the name is a linter.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the name a filename or a directory?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree it's confusing. But "the name is a path relative to the config" above refers to those names, this one refers to the more nested names. Taking another pass.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PTAL

#' " 1. Numeric or unnamed list elements are line numbers to be excluded.",
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
#' " 2. Named list elements are line numbers to be excluded for specific linters.",
#' " For convenience, a vector can be used in place of a list whenever it would not introduce ambiguity, e.g.",
#' " a character vector of files to exclude or a vector of lines to exclude.",
#' NULL
#' )
exclude <- function(lints, exclusions = settings$exclusions, linter_names = NULL, ...) {
if (length(lints) <= 0L) {
Expand Down
92 changes: 92 additions & 0 deletions R/settings.R
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ read_settings <- function(filename) {
}

config <- read_config_file(config_file)
validate_config_file(config, config_file, default_settings)

for (setting in names(default_settings)) {
value <- get_setting(setting, config, default_settings)
Expand Down Expand Up @@ -77,6 +78,97 @@ read_config_file <- function(config_file) {
config
}

validate_config_file <- function(config, config_file, defaults) {
matched <- names(config) %in% names(defaults)
if (!all(matched)) {
warning("Found unused settings in config '", config_file, "': ", toString(names(config)[!matched]))
}

validate_regex(config,
c("exclude", "exclude_next", "exclude_start", "exclude_end", "exclude_linter", "exclude_linter_sep")
)
validate_character_string(config, c("encoding", "cache_directory", "comment_token"))
validate_true_false(config, c("comment_bot", "error_on_lint"))
validate_linters(config$linters)
validate_exclusions(config$exclusions)
}

is_character_string <- function(x) is.character(x) && length(x) == 1L && !is.na(x)
is_valid_regex <- function(str) !inherits(tryCatch(grepl(str, ""), condition = identity), "condition")
is_single_regex <- function(x) is_character_string(x) && is_valid_regex(x)
is_true_false <- function(x) is.logical(x) && length(x) == 1L && !is.na(x)
AshesITR marked this conversation as resolved.
Show resolved Hide resolved

validate_keys <- function(config, keys, test, what) {
for (key in keys) {
val <- config[[key]]
if (is.null(val)) {
next
}
if (!test(val)) {
stop("Setting '", key, "' should be ", what, ", not '", toString(val), "'.")
}
}
}

validate_regex <- function(config, keys) {
validate_keys(config, keys, is_single_regex, "a single regular expression")
}

validate_character_string <- function(config, keys) {
validate_keys(config, keys, is_character_string, "a character string")
}

validate_true_false <- function(config, keys) {
validate_keys(config, keys, is_true_false, "TRUE or FALSE")
}

validate_linters <- function(linters) {
if (is.null(linters)) {
return(invisible())
}

is_linters <- vapply(linters, is_linter, logical(1L))
if (!all(is_linters)) {
stop(
"Setting 'linters' should be a list of linters, but found non-linters at elements ",
toString(which(!is_linters)), "."
)
}
}

validate_exclusions <- function(exclusions) {
if (is.null(exclusions)) {
return(invisible())
}

exclusion_names <- names2(exclusions)
has_names <- exclusion_names != ""
unnamed_is_string <-
vapply(exclusions[!has_names], function(x) is.character(x) && length(x) == 1L && !is.na(x), logical(1L))
if (!all(unnamed_is_string)) {
stop(
"Unnamed entries of setting 'exclusions' should be strings naming files or directories, check entries: ",
toString(which(!has_names)[!unnamed_is_string]), "."
)
}
for (ii in which(has_names)) validate_named_exclusion(exclusions, ii)
}

validate_named_exclusion <- function(exclusions, idx) {
entry <- exclusions[[idx]]
if (is.list(entry)) {
valid_entry <- vapply(entry, function(x) is.numeric(x) && !anyNA(x), logical(1L))
} else {
valid_entry <- is.numeric(entry) && !anyNA(entry)
}
if (!all(valid_entry)) {
stop(
"Named entries of setting 'exclusions' should designate line numbers for exclusion, ",
"check exclusion: ", idx, "."
)
}
}

lintr_option <- function(setting, default = NULL) getOption(paste0("lintr.", setting), default)

get_setting <- function(setting, config, defaults) {
Expand Down
2 changes: 1 addition & 1 deletion R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ default_undesirable_operators <- all_undesirable_operators[names(all_undesirable
#' - `exclude`: pattern used to exclude a line of code
#' - `exclude_start`, `exclude_end`: patterns used to mark start and end of the code block to exclude
#' - `exclude_linter`, `exclude_linter_sep`: patterns used to exclude linters
#' - `exclusions`:a list of files to exclude
#' - `exclusions`: a list of exclusions, see [exclude()] for a complete description of valid values.
#' - `cache_directory`: location of cache directory
#' - `comment_token`: a GitHub token character
#' - `comment_bot`: decides if lintr comment bot on GitHub can comment on commits
Expand Down
2 changes: 1 addition & 1 deletion man/default_settings.Rd

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

19 changes: 14 additions & 5 deletions man/exclude.Rd

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

6 changes: 5 additions & 1 deletion tests/testthat/test-lint_package.R
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,11 @@ test_that("package using .lintr.R config lints correctly", {

# config produces unused variables
withr::local_options(lintr.linter_file = "lintr_test_config_extraneous")
expect_length(lint_package(r_config_pkg), 2L)
expect_warning(
expect_length(lint_package(r_config_pkg), 2L),
"Found unused settings in config",
fixed = TRUE
)

# DCF is preferred if multiple matched configs
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
withr::local_options(lintr.linter_file = "lintr_test_config_conflict")
Expand Down
91 changes: 91 additions & 0 deletions tests/testthat/test-settings.R
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,94 @@ test_that("it has a smart default for encodings", {
lintr:::read_settings(pkg_file)
expect_identical(settings$encoding, "ISO8859-1")
})

test_that("validate_config_file() detects improperly-formed settings", {
.lintr <- withr::local_tempfile()
withr::local_options(lintr.linter_file = .lintr)
withr::local_dir(withr::local_tempdir())

writeLines("asdf: 1", .lintr)
expect_warning(lint_dir(), "Found unused settings in config", fixed = TRUE)

writeLines("a=1", "aaa.R")
writeLines(c('exclusions: list("aaa.R")', "asdf: 1"), .lintr)
expect_warning(lint_dir(), "Found unused settings in config", fixed = TRUE)

writeLines("encoding: FALSE", .lintr)
expect_error(lint_dir(), "Setting 'encoding' should be a character string, not 'FALSE'", fixed = TRUE)

writeLines("encoding: NA_character_", .lintr)
expect_error(lint_dir(), "Setting 'encoding' should be a character string, not 'NA'", fixed = TRUE)

writeLines("encoding: letters", .lintr)
expect_error(lint_dir(), "Setting 'encoding' should be a character string, not '[a-z, ]+'")
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved

writeLines("exclude: FALSE", .lintr)
expect_error(lint_dir(), "Setting 'exclude' should be a single regular expression, not 'FALSE'", fixed = TRUE)

writeLines(c('exclusions: list("aaa.R")', "exclude: FALSE"), .lintr)
expect_error(lint_dir(), "Setting 'exclude' should be a single regular expression, not 'FALSE'", fixed = TRUE)

writeLines('exclude: "("', .lintr)
expect_error(lint_dir(), "Setting 'exclude' should be a single regular expression, not '('", fixed = TRUE)

writeLines('comment_bot: "a"', .lintr)
expect_error(lint_dir(), "Setting 'comment_bot' should be TRUE or FALSE, not 'a'", fixed = TRUE)

writeLines("comment_bot: NA", .lintr)
expect_error(lint_dir(), "Setting 'comment_bot' should be TRUE or FALSE, not 'NA'", fixed = TRUE)

writeLines("comment_bot: c(TRUE, FALSE)", .lintr)
expect_error(lint_dir(), "Setting 'comment_bot' should be TRUE or FALSE, not 'TRUE, FALSE'", fixed = TRUE)

writeLines("linters: list(1)", .lintr)
expect_error(lint_dir(), "Setting 'linters' should be a list of linters", fixed = TRUE)

writeLines("linters: list(assignment_linter(), 1)", .lintr)
expect_error(lint_dir(), "Setting 'linters' should be a list of linters", fixed = TRUE)

writeLines("exclusions: list(1L)", .lintr)
expect_error(lint_dir(), "Unnamed entries of setting 'exclusions' should be strings", fixed = TRUE)

writeLines('exclusions: list("aaa.R", 1L)', .lintr)
expect_error(lint_dir(), "Unnamed entries of setting 'exclusions' should be strings", fixed = TRUE)

writeLines("exclusions: list(letters)", .lintr)
expect_error(lint_dir(), "Unnamed entries of setting 'exclusions' should be strings", fixed = TRUE)

writeLines("exclusions: list(NA_character_)", .lintr)
expect_error(lint_dir(), "Unnamed entries of setting 'exclusions' should be strings", fixed = TRUE)

writeLines('exclusions: list(aaa.R = "abc")', .lintr)
expect_error(lint_dir(), "Named entries of setting 'exclusions' should designate line numbers", fixed = TRUE)

writeLines("exclusions: list(aaa.R = NA_integer_)", .lintr)
expect_error(lint_dir(), "Named entries of setting 'exclusions' should designate line numbers", fixed = TRUE)

writeLines('exclusions: list(aaa.R = list("abc"))', .lintr)
expect_error(lint_dir(), "Named entries of setting 'exclusions' should designate line numbers", fixed = TRUE)

writeLines("exclusions: list(aaa.R = list(NA_integer_))", .lintr)
expect_error(lint_dir(), "Named entries of setting 'exclusions' should designate line numbers", fixed = TRUE)

writeLines('exclusions: list(aaa.R = list(assignment_linter = "abc"))', .lintr)
expect_error(lint_dir(), "Named entries of setting 'exclusions' should designate line numbers", fixed = TRUE)

writeLines("exclusions: list(aaa.R = list(assignment_linter = NA_integer_))", .lintr)
expect_error(lint_dir(), "Named entries of setting 'exclusions' should designate line numbers", fixed = TRUE)
})

test_that("exclusions can be a character vector", {
withr::local_dir(withr::local_tempdir())
# exclusions are relative to dirname(.lintr), so must create it here
.lintr <- withr::local_tempfile(tmpdir = getwd())
withr::local_options(lintr.linter_file = .lintr)

writeLines('exclusions: "aaa.R"', .lintr)
writeLines("a=1", "aaa.R")
writeLines("b<-1", "bbb.R")
expect_length(lint_dir(), 1L)

writeLines('exclusions: c("aaa.R", "bbb.R")', .lintr)
expect_length(lint_dir(), 0L)
})
Loading