|
| 1 | +#' Brace linter |
| 2 | +#' |
| 3 | +#' Perform various style checks related to placement and spacing of curly braces: |
| 4 | +#' |
| 5 | +#' - Opening curly braces are never on their own line and are always followed by a newline. |
| 6 | +#' - Opening curly braces have a space before them. |
| 7 | +#' - Closing curly braces are on their own line unless they are followed by an `else`. |
| 8 | +#' - Closing curly braces in `if` conditions are on the same line as the corresponding `else`. |
| 9 | +#' - Either both or neither branch in `if`/`else` use curly braces, i.e., either both branches use `{...}` or neither |
| 10 | +#' does. |
| 11 | +#' - Functions spanning multiple lines use curly braces. |
| 12 | +#' |
| 13 | +#' @param allow_single_line if `TRUE`, allow an open and closed curly pair on the same line. |
| 14 | +#' |
| 15 | +#' @evalRd rd_tags("brace_linter") |
| 16 | +#' @seealso [linters] for a complete list of linters available in lintr. \cr |
| 17 | +#' <https://style.tidyverse.org/syntax.html#indenting> \cr |
| 18 | +#' <https://style.tidyverse.org/syntax.html#if-statements> |
| 19 | +#' @export |
| 20 | +brace_linter <- function(allow_single_line = FALSE) { |
| 21 | + Linter(function(source_expression) { |
| 22 | + if (length(source_expression$xml_parsed_content) == 0L) { |
| 23 | + return(list()) |
| 24 | + } |
| 25 | + |
| 26 | + lints <- list() |
| 27 | + |
| 28 | + xp_cond_open <- xp_and(c( |
| 29 | + # matching } is on same line |
| 30 | + if (isTRUE(allow_single_line)) { |
| 31 | + "(@line1 != following-sibling::OP-LEFT-BRACE/@line1)" |
| 32 | + }, |
| 33 | + # double curly |
| 34 | + "not( |
| 35 | + (@line1 = parent::expr/preceding-sibling::OP-LEFT-BRACE/@line1) or |
| 36 | + (@line1 = following-sibling::expr/OP-LEFT-BRACE/@line1) |
| 37 | + )" |
| 38 | + )) |
| 39 | + |
| 40 | + # TODO (AshesITR): if c_style_braces is TRUE, invert the preceding-sibling condition |
| 41 | + xp_open_curly <- glue::glue("//OP-LEFT-BRACE[ |
| 42 | + { xp_cond_open } and ( |
| 43 | + not(@line1 = parent::expr/preceding-sibling::*/@line2) or |
| 44 | + @line1 = following-sibling::*[1][not(self::COMMENT)]/@line1 |
| 45 | + ) |
| 46 | + ]") |
| 47 | + |
| 48 | + lints <- c(lints, lapply( |
| 49 | + xml2::xml_find_all(source_expression$xml_parsed_content, xp_open_curly), |
| 50 | + xml_nodes_to_lint, |
| 51 | + source_file = source_expression, |
| 52 | + lint_message = paste( |
| 53 | + "Opening curly braces should never go on their own line and", |
| 54 | + "should always be followed by a new line." |
| 55 | + ) |
| 56 | + )) |
| 57 | + |
| 58 | + xp_open_preceding <- "parent::expr/preceding-sibling::*[1][self::OP-RIGHT-PAREN or self::ELSE or self::REPEAT]" |
| 59 | + |
| 60 | + xp_paren_brace <- glue::glue("//OP-LEFT-BRACE[ |
| 61 | + @line1 = { xp_open_preceding }/@line1 |
| 62 | + and |
| 63 | + @col1 = { xp_open_preceding }/@col2 + 1 |
| 64 | + ]") |
| 65 | + |
| 66 | + lints <- c(lints, lapply( |
| 67 | + xml2::xml_find_all(source_expression$xml_parsed_content, xp_paren_brace), |
| 68 | + xml_nodes_to_lint, |
| 69 | + source_file = source_expression, |
| 70 | + lint_message = "There should be a space before an opening curly brace." |
| 71 | + )) |
| 72 | + |
| 73 | + xp_cond_closed <- xp_and(c( |
| 74 | + # matching { is on same line |
| 75 | + if (isTRUE(allow_single_line)) { |
| 76 | + "(@line1 != preceding-sibling::OP-LEFT-BRACE/@line1)" |
| 77 | + }, |
| 78 | + # immediately followed by ",", "]" or ")" |
| 79 | + "not( |
| 80 | + @line1 = ancestor::expr/following-sibling::*[1][ |
| 81 | + self::OP-COMMA or self::OP-RIGHT-BRACKET or self::OP-RIGHT-PAREN |
| 82 | + ]/@line1 |
| 83 | + )", |
| 84 | + # double curly |
| 85 | + "not( |
| 86 | + (@line1 = parent::expr/following-sibling::OP-RIGHT-BRACE/@line1) or |
| 87 | + (@line1 = preceding-sibling::expr/OP-RIGHT-BRACE/@line1) |
| 88 | + )" |
| 89 | + )) |
| 90 | + |
| 91 | + # TODO (AshesITR): if c_style_braces is TRUE, skip the not(ELSE) condition |
| 92 | + xp_closed_curly <- glue::glue("//OP-RIGHT-BRACE[ |
| 93 | + { xp_cond_closed } and ( |
| 94 | + (@line1 = preceding-sibling::*[1]/@line2) or |
| 95 | + (@line1 = parent::expr/following-sibling::*[1][not(self::ELSE)]/@line1) |
| 96 | + ) |
| 97 | + ]") |
| 98 | + |
| 99 | + lints <- c(lints, lapply( |
| 100 | + xml2::xml_find_all(source_expression$xml_parsed_content, xp_closed_curly), |
| 101 | + xml_nodes_to_lint, |
| 102 | + source_file = source_expression, |
| 103 | + lint_message = paste( |
| 104 | + "Closing curly-braces should always be on their own line,", |
| 105 | + "unless they are followed by an else." |
| 106 | + ) |
| 107 | + )) |
| 108 | + |
| 109 | + xp_else_closed_curly <- "preceding-sibling::IF/following-sibling::expr[2]/OP-RIGHT-BRACE" |
| 110 | + # need to (?) repeat previous_curly_path since != will return true if there is |
| 111 | + # no such node. ditto for approach with not(@line1 = ...). |
| 112 | + # TODO (AshesITR): if c_style_braces is TRUE, this needs to be @line2 + 1 |
| 113 | + xp_else_same_line <- glue::glue("//ELSE[{xp_else_closed_curly} and @line1 != {xp_else_closed_curly}/@line2]") |
| 114 | + |
| 115 | + lints <- c(lints, lapply( |
| 116 | + xml2::xml_find_all(source_expression$xml_parsed_content, xp_else_same_line), |
| 117 | + xml_nodes_to_lint, |
| 118 | + source_file = source_expression, |
| 119 | + lint_message = "`else` should come on the same line as the previous `}`." |
| 120 | + )) |
| 121 | + |
| 122 | + xp_function_brace <- "//expr[FUNCTION and @line1 != @line2 and not(expr[OP-LEFT-BRACE])]" |
| 123 | + |
| 124 | + lints <- c(lints, lapply( |
| 125 | + xml2::xml_find_all(source_expression$xml_parsed_content, xp_function_brace), |
| 126 | + xml_nodes_to_lint, |
| 127 | + source_file = source_expression, |
| 128 | + lint_message = "Any function spanning multiple lines should use curly braces." |
| 129 | + )) |
| 130 | + |
| 131 | + # if (x) { ... } else if (y) { ... } else { ... } is OK; fully exact pairing |
| 132 | + # of if/else would require this to be |
| 133 | + # if (x) { ... } else { if (y) { ... } else { ... } } since there's no |
| 134 | + # elif operator/token in R, which is pretty unseemly |
| 135 | + xp_if_else_match_brace <- " |
| 136 | + //IF[ |
| 137 | + following-sibling::expr[2][OP-LEFT-BRACE] |
| 138 | + and following-sibling::ELSE |
| 139 | + /following-sibling::expr[1][not(OP-LEFT-BRACE or IF/following-sibling::expr[2][OP-LEFT-BRACE])] |
| 140 | + ] |
| 141 | +
|
| 142 | + | |
| 143 | +
|
| 144 | + //ELSE[ |
| 145 | + following-sibling::expr[1][OP-LEFT-BRACE] |
| 146 | + and preceding-sibling::IF/following-sibling::expr[2][not(OP-LEFT-BRACE)] |
| 147 | + ] |
| 148 | + " |
| 149 | + |
| 150 | + lints <- c(lints, lapply( |
| 151 | + xml2::xml_find_all(source_expression$xml_parsed_content, xp_if_else_match_brace), |
| 152 | + xml_nodes_to_lint, |
| 153 | + source_file = source_expression, |
| 154 | + lint_message = "Either both or neither branch in `if`/`else` should use curly braces." |
| 155 | + )) |
| 156 | + |
| 157 | + lints |
| 158 | + }) |
| 159 | +} |
0 commit comments