-
Notifications
You must be signed in to change notification settings - Fork 186
implement indentation_linter #1411
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
Merged
Merged
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
c1e1fa1
implement indentation_linter
AshesITR c4e1102
xml2::*
AshesITR 00c56ab
fix tests
AshesITR e1841cc
fix range start (off by one)
AshesITR 30263f9
ignore indentation level within string constants
AshesITR c20fbaa
fix lints and false positives, handle nested indentation changes
AshesITR deb0a8c
de-lint package, explicit branch for no lints
AshesITR fd11ed8
adapt indent suppression condition
AshesITR 73fed7a
change indentation end path for $function calls
AshesITR 8ece83a
return non-empty file_lines even if no R source is present
AshesITR 908674d
ensure Rmd is detected
AshesITR f6c14e5
.Rmd
AshesITR 328e0c1
Merge branch 'main' into fix/1415-empty-r-source
AshesITR 2131d2b
add use_hybrid_indent = TRUE
AshesITR d0ef678
except hanging indent if following expr is a code block
AshesITR 075fa98
smarter hanging indent detection for use_hybrid_indent
AshesITR fe80a5e
suppress redundant lints
AshesITR 8a7acfb
Merge branch 'main' into feature/indentation-linter
AshesITR 7831de8
Merge branch 'fix/1415-empty-r-source' into feature/indentation-linter
AshesITR a820b6e
update tests
AshesITR 9c19d71
de-lint
AshesITR 52aef2c
Merge branch 'main' into feature/indentation-linter
AshesITR accb884
Merge branch 'main' into feature/indentation-linter
AshesITR 5f1c5db
Merge branch 'main' into feature/indentation-linter
MichaelChirico 04d28b0
move around NEWS
MichaelChirico 76b16da
if condition styling
MichaelChirico ada0d75
use TODO for consistency/searchability
MichaelChirico f119656
address review comments
AshesITR 0ff0394
NEWS wording
MichaelChirico 0910ccd
add more tests, refactor to file-level because of a systematic proble…
AshesITR 2cc351e
Merge branch 'main' into feature/indentation-linter
AshesITR 5843e95
refac use_hybrid_indent -> hanging_indent_style
AshesITR f701869
allow compare_branches.R to use custom parameters if desired
AshesITR 2bfc8ee
improve docs
AshesITR d20d740
de-lint
AshesITR b23d2ec
Merge branch 'main' into feature/indentation-linter
MichaelChirico File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
#' Check that indentation is consistent | ||
#' | ||
#' @param indent Number of spaces, that a code block should be indented by relative to its parent code block. | ||
#' Used for multi-line code blocks (`{ ... }`), function calls (`( ... )`) and extractions (`[ ... ]`, `[[ ... ]]`). | ||
#' Defaults to 2. | ||
#' @param use_hybrid_indent Require a block indent for multi-line function calls if there are only unnamed arguments | ||
#' in the first line? | ||
#' ```r | ||
#' # complies only with use_hybrid_indent = TRUE: | ||
#' map(x, f, | ||
#' additional_arg = 42 | ||
#' ) | ||
#' | ||
#' # complies only with use_hybrid_indent = FALSE: | ||
#' map(x, f, | ||
#' additional_arg = 42 | ||
#' ) | ||
#' | ||
#' # always complies | ||
AshesITR marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#' map(x, f, | ||
#' additional_arg = 42) | ||
#' ``` | ||
#' | ||
#' @examples | ||
#' # will produce lints | ||
#' lint( | ||
#' text = "if (TRUE) {\n1 + 1\n}", | ||
#' linters = indentation_linter() | ||
#' ) | ||
#' | ||
#' lint( | ||
#' text = "if (TRUE) {\n 1 + 1\n}", | ||
#' linters = indentation_linter() | ||
#' ) | ||
#' | ||
#' lint( | ||
#' text = "map(x, f,\n additional_arg = 42\n)", | ||
#' linters = indentation_linter(use_hybrid_indent = FALSE) | ||
#' ) | ||
#' | ||
#' # okay | ||
#' lint( | ||
#' text = "map(x, f,\n additional_arg = 42\n)", | ||
#' linters = indentation_linter() | ||
#' ) | ||
#' | ||
#' lint( | ||
#' text = "if (TRUE) {\n 1 + 1\n}", | ||
#' linters = indentation_linter(indent = 4) | ||
#' ) | ||
#' | ||
#' @evalRd rd_tags("indentation_linter") | ||
AshesITR marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#' @seealso [linters] for a complete list of linters available in lintr. \cr | ||
#' <https://style.tidyverse.org/syntax.html#indenting> | ||
#' | ||
#' @export | ||
indentation_linter <- function(indent = 2L, use_hybrid_indent = TRUE) { | ||
paren_tokens_left <- c("OP-LEFT-BRACE", "OP-LEFT-PAREN", "OP-LEFT-BRACKET", "LBB") | ||
paren_tokens_right <- c("OP-RIGHT-BRACE", "OP-RIGHT-PAREN", "OP-RIGHT-BRACKET", "OP-RIGHT-BRACKET") | ||
infix_tokens <- setdiff(infix_metadata$xml_tag, c("OP-LEFT-BRACE", "OP-COMMA", paren_tokens_left)) | ||
no_paren_keywords <- c("ELSE", "REPEAT") | ||
keyword_tokens <- c("FUNCTION", "IF", "FOR", "WHILE") | ||
|
||
xp_last_on_line <- "@line1 != following-sibling::*[not(self::COMMENT)][1]/@line1" | ||
|
||
if (use_hybrid_indent) { | ||
xp_is_not_hanging <- paste( | ||
c( | ||
glue::glue( | ||
"self::{paren_tokens_left}/following-sibling::{paren_tokens_right}[@line1 > preceding-sibling::*[1]/@line2]" | ||
), | ||
glue::glue("self::*[{xp_and(paste0('not(self::', paren_tokens_left, ')'))} and {xp_last_on_line}]") | ||
), | ||
collapse = " | " | ||
) | ||
} else { | ||
xp_is_not_hanging <- glue::glue("self::*[{xp_last_on_line}]") | ||
} | ||
|
||
xp_block_ends <- paste0( | ||
"number(", | ||
paste( | ||
c( | ||
glue::glue("self::{paren_tokens_left}/following-sibling::{paren_tokens_right}/preceding-sibling::*[1]/@line2"), | ||
glue::glue("self::*[{xp_and(paste0('not(self::', paren_tokens_left, ')'))}]/following-sibling::SYMBOL_FUNCTION_CALL/ | ||
AshesITR marked this conversation as resolved.
Show resolved
Hide resolved
|
||
parent::expr/following-sibling::expr[1]/@line2"), | ||
glue::glue("self::*[ | ||
{xp_and(paste0('not(self::', paren_tokens_left, ')'))} and | ||
not(following-sibling::SYMBOL_FUNCTION_CALL) | ||
]/following-sibling::*[1]/@line2") | ||
), | ||
collapse = " | " | ||
), | ||
")" | ||
) | ||
|
||
xp_indent_changes <- paste( | ||
c( | ||
glue::glue("//{paren_tokens_left}[not(@line1 = following-sibling::expr[ | ||
@line2 > @line1 and | ||
({xp_or(paste0('descendant::', paren_tokens_left, '[', xp_last_on_line, ']'))}) | ||
]/@line1)]"), | ||
glue::glue("//{infix_tokens}[{xp_last_on_line}]"), | ||
glue::glue("//{no_paren_keywords}[{xp_last_on_line}]"), | ||
glue::glue("//{keyword_tokens}/following-sibling::OP-RIGHT-PAREN[ | ||
{xp_last_on_line} and | ||
not(following-sibling::expr[1][OP-LEFT-BRACE]) | ||
]") | ||
), | ||
collapse = " | " | ||
) | ||
|
||
xp_multiline_string <- "//STR_CONST[@line1 < @line2]" | ||
|
||
Linter(function(source_expression) { | ||
if (!is_lint_level(source_expression, "expression")) { | ||
return(list()) | ||
} | ||
|
||
xml <- source_expression$xml_parsed_content | ||
# Indentation increases by 1 for: | ||
# - { } blocks that span multiple lines | ||
# - ( ), [ ], or [[ ]] calls that span multiple lines | ||
# + if a token follows (, a hanging indent is required until ) | ||
# + if there is no token following ( on the same line, a block indent is required until ) | ||
# - binary operators where the second arguments starts on a new line | ||
|
||
indent_levels <- rex::re_matches(source_expression$lines, rex::rex(start, any_spaces), locations = TRUE)[, "end"] | ||
expected_indent_levels <- integer(length(indent_levels)) | ||
is_hanging <- logical(length(indent_levels)) | ||
|
||
indent_changes <- xml2::xml_find_all(xml, xp_indent_changes) | ||
for (change in indent_changes) { | ||
change_starts_hanging <- length(xml2::xml_find_first(change, xp_is_not_hanging)) == 0L | ||
change_begin <- as.integer(xml2::xml_attr(change, "line1")) + 1L | ||
change_end <- xml2::xml_find_num(change, xp_block_ends) | ||
if (change_begin <= change_end) { | ||
to_indent <- seq(from = change_begin, to = change_end) - source_expression$line + 1L | ||
if (change_starts_hanging) { | ||
expected_indent_levels[to_indent] <- as.integer(xml2::xml_attr(change, "col2")) | ||
is_hanging[to_indent] <- TRUE | ||
} else { | ||
expected_indent_levels[to_indent] <- expected_indent_levels[to_indent] + indent | ||
is_hanging[to_indent] <- FALSE | ||
} | ||
} | ||
} | ||
|
||
in_str_const <- logical(length(indent_levels)) | ||
multiline_strings <- xml2::xml_find_all(xml, xp_multiline_string) | ||
for (string in multiline_strings) { | ||
is_in_str <- seq( | ||
from = as.integer(xml2::xml_attr(string, "line1")) + 1L, | ||
to = as.integer(xml2::xml_attr(string, "line2")) | ||
) - source_expression$line + 1L | ||
in_str_const[is_in_str] <- TRUE | ||
} | ||
|
||
# Only lint non-empty lines if the indentation level doesn't match. | ||
bad_lines <- which(indent_levels != expected_indent_levels & nzchar(source_expression$lines) & !in_str_const) | ||
if (length(bad_lines)) { | ||
# Suppress consecutive lints with the same indentation difference, to not generate an excessive number of lints | ||
is_consecutive_lint <- c(FALSE, diff(bad_lines) == 1L) | ||
indent_diff <- expected_indent_levels[bad_lines] - indent_levels[bad_lines] | ||
is_same_diff <- c(FALSE, diff(indent_diff) == 0L) | ||
|
||
bad_lines <- bad_lines[!(is_consecutive_lint & is_same_diff)] | ||
|
||
lint_messages <- sprintf( | ||
"%s should be %d spaces but is %d spaces.", | ||
ifelse(is_hanging[bad_lines], "Hanging indent", "Indentation"), | ||
expected_indent_levels[bad_lines], | ||
indent_levels[bad_lines] | ||
) | ||
lint_lines <- unname(as.integer(names(source_expression$lines)[bad_lines])) | ||
lint_ranges <- cbind( | ||
pmin(expected_indent_levels[bad_lines] + 1L, indent_levels[bad_lines]), | ||
pmax(expected_indent_levels[bad_lines], indent_levels[bad_lines]) | ||
) | ||
Map( | ||
Lint, | ||
filename = source_expression$filename, | ||
line_number = lint_lines, | ||
column_number = indent_levels[bad_lines], | ||
type = "style", | ||
message = lint_messages, | ||
line = unname(source_expression$lines[bad_lines]), | ||
ranges = apply(lint_ranges, 1L, list, simplify = FALSE) | ||
) | ||
} else { | ||
list() | ||
} | ||
}) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks OK, is it relevant to this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, is this fixing an existing indentation_linter violation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC I didn't understand the original code very well and it's pretty terrible in terms of performance.
Also, not sure if indenting after
<-
should be encouraged. The old code would be more readable ifMap(
were on the preceding line.It's such a small change that I wouldn't make a separate PR for this. LMK if I should revert.
Created on 2022-10-16 by the reprex package (v2.0.1)