Skip to content

Commit d665ee8

Browse files
Merge pull request #279 from lorenzwalthert/use-next-non-comment
- Fix token insertion / comment interaction (#279).
2 parents 105933c + 5413eb2 commit d665ee8

30 files changed

+1464
-39
lines changed

R/expr-is.R

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,32 @@ is_function_dec <- function(pd) {
2828
if (is.null(pd)) return(FALSE)
2929
pd$token[1] == "FUNCTION"
3030
}
31+
32+
33+
contains_else_expr <- function(pd) {
34+
any(pd$token == "ELSE")
35+
}
36+
37+
#' Check whether an else expression needs braces
38+
#'
39+
#' Checks whether an else expression in a nest needs braces. Note that for
40+
#' if-else-if expressions, there is no need to add braces since the if in
41+
#' else-if will be visited separately with the visitor. This applies to all
42+
#' conditional statents with more than one alternative.
43+
#' @param pd A parse table
44+
contains_else_expr_that_needs_braces <- function(pd) {
45+
else_idx <- which(pd$token == "ELSE")
46+
if (length(else_idx) > 0) {
47+
non_comment_after_else <- next_non_comment(pd, else_idx)
48+
sub_expr <- pd$child[[non_comment_after_else]]
49+
# needs braces if NOT if_condition, NOT curly expr
50+
!is_cond_expr(sub_expr) && !is_curly_expr(sub_expr)
51+
} else {
52+
FALSE
53+
}
54+
}
55+
56+
57+
is_cond_expr <- function(pd) {
58+
pd$token[1] == "IF"
59+
}

R/indent.R

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,21 @@ indent_without_paren_for_while_fun <- function(pd, indent_by) {
7878
#' @describeIn update_indention Is used to indent if and if-else statements.
7979
#' @importFrom rlang seq2
8080
indent_without_paren_if_else <- function(pd, indent_by) {
81+
expr_after_if <- next_non_comment(pd, which(pd$token == "')'")[1])
8182
has_if_without_curly <-
82-
pd$token[1] %in% c("IF", "WHILE") && pd$child[[5]]$token[1] != "'{'"
83+
pd$token[1] %in% "IF" && pd$child[[expr_after_if]]$token[1] != "'{'"
8384
if (has_if_without_curly) {
84-
pd$indent[5] <- indent_by
85+
pd$indent[expr_after_if] <- indent_by
8586
}
8687

88+
else_idx <- which(pd$token == "ELSE")
89+
expr_after_else_idx <- next_non_comment(pd, else_idx)
8790
has_else_without_curly_or_else_chid <-
8891
any(pd$token == "ELSE") &&
89-
pd$child[[7]]$token[1] != "'{'" &&
90-
pd$child[[7]]$token[1] != "IF"
92+
pd$child[[expr_after_else_idx]]$token[1] != "'{'" &&
93+
pd$child[[expr_after_else_idx]]$token[1] != "IF"
9194
if (has_else_without_curly_or_else_chid) {
92-
pd$indent[seq(7, nrow(pd))] <- indent_by
95+
pd$indent[seq(else_idx + 1, nrow(pd))] <- indent_by
9396
}
9497
pd
9598
}

R/rules-other.R

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ add_brackets_in_pipe_one <- function(pd, pos) {
1010
new_pos_ids <- create_pos_ids(pd$child[[next_non_comment]], 1, after = TRUE, n = 2)
1111
new_pd <- create_tokens(
1212
tokens = c("'('", "')'"), texts = c("(", ")"), pos_ids = new_pos_ids,
13-
lag_newlines = rep(0, 2))
13+
lag_newlines = rep(0, 2)
14+
)
1415
pd$child[[next_non_comment]] <-
1516
bind_rows(pd$child[[next_non_comment]], new_pd) %>%
1617
arrange(pos_id)
@@ -26,26 +27,81 @@ add_brackets_in_pipe_one <- function(pd, pos) {
2627
#' @param indent_by The amont of spaces used to indent an expression in curly
2728
#' braces. Used for unindention.
2829
wrap_if_else_multi_line_in_curly <- function(pd, indent_by = 2) {
29-
if (pd$token[1] == "IF" &&
30-
pd_is_multi_line(pd) &&
31-
!is_curly_expr(pd$child[[5]])) {
32-
pd$lag_newlines[5] <- 0L
33-
pd$spaces[4] <- 1L
34-
pd$spaces[5] <- 0L
30+
if (is_cond_expr(pd)) {
31+
pd <- pd %>%
32+
wrap_if_multiline_curly(indent_by) %>%
33+
wrap_else_multiline_curly(indent_by)
34+
}
35+
pd
36+
}
37+
38+
39+
wrap_if_multiline_curly <- function(pd, indent_by) {
40+
if (if_part_requires_braces(pd)) {
41+
closing_brace_ind <- which(pd$token == "')'")[1]
42+
pd$spaces[closing_brace_ind] <- 1L
43+
44+
to_be_wrapped_expr_with_child <- next_non_comment(
45+
pd,
46+
which(pd$token == "')'")[1]
47+
)
48+
49+
all_to_be_wrapped_ind <- seq2(
50+
closing_brace_ind + 1L, to_be_wrapped_expr_with_child
51+
)
52+
53+
pd <- wrap_subexpr_in_curly(
54+
pd, all_to_be_wrapped_ind, indent_by
55+
)
56+
3557
if (nrow(pd) > 5) pd$lag_newlines[6] <- 0L
36-
pd$indent[5] <- pd$indent[5] - indent_by
37-
pd$child[[5]] <- wrap_expr_in_curly(pd$child[[5]], stretch_out = TRUE)
38-
pd$multi_line[5] <- TRUE
3958
}
40-
if (nrow(pd) > 6 &&
41-
(pd$token[6] == "ELSE" && pd_is_multi_line(pd)) &&
42-
pd$child[[7]]$token != "IF" &&
43-
!is_curly_expr(pd$child[[7]])) {
44-
pd$spaces[6] <- 1L
45-
pd$spaces[7] <- 0L
46-
pd$indent[7] <- pd$indent[7] - indent_by
47-
pd$lag_newlines[7] <- 0L
48-
pd$child[[7]] <- wrap_expr_in_curly(pd$child[[7]], stretch_out = TRUE)
59+
pd
60+
}
61+
62+
wrap_else_multiline_curly <- function(pd, indent_by = 2) {
63+
if (contains_else_expr(pd) &&
64+
pd_is_multi_line(pd) &&
65+
contains_else_expr_that_needs_braces(pd)) {
66+
else_idx <- which(pd$token == "ELSE")
67+
pd$spaces[else_idx] <- 1L
68+
all_to_be_wrapped_ind <- seq2(else_idx + 1L, nrow(pd))
69+
70+
pd <- wrap_subexpr_in_curly(
71+
pd, all_to_be_wrapped_ind, indent_by
72+
)
4973
}
5074
pd
5175
}
76+
77+
#' Wrap a sub-expression in curly braces
78+
#'
79+
#' Wraps some rows of a parse table into a sub-expression.
80+
#' @inheritParams wrap_if_else_multi_line_in_curly
81+
#' @param ind_to_be_wrapped The indices of the rows that should be wrapped
82+
#' into a new expression.
83+
wrap_subexpr_in_curly <- function(pd,
84+
ind_to_be_wrapped,
85+
indent_by) {
86+
to_be_wrapped_starts_with_comment <-
87+
pd$token[ind_to_be_wrapped[1]] == "COMMENT"
88+
new_expr <- wrap_expr_in_curly(
89+
pd[ind_to_be_wrapped, ],
90+
stretch_out = c(!to_be_wrapped_starts_with_comment, TRUE)
91+
)
92+
new_expr$indent <- pd$indent[last(ind_to_be_wrapped)] - indent_by
93+
new_expr_in_expr <- new_expr %>%
94+
wrap_expr_in_expr() %>%
95+
remove_attributes(c("token_before", "token_after"))
96+
97+
pd %>%
98+
slice(-ind_to_be_wrapped) %>%
99+
bind_rows(new_expr_in_expr) %>%
100+
set_multi_line() %>%
101+
arrange(pos_id)
102+
}
103+
104+
if_part_requires_braces <- function(pd) {
105+
pd_is_multi_line(pd) &&
106+
!is_curly_expr(pd$child[[next_non_comment(pd, which(pd$token == "')'")[1])]])
107+
}

R/serialized_tests.R

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,40 @@ stop_insufficient_r_version <- function() {
211211
"since data.tree not available. Needs at least R version 3.2."
212212
), call. = FALSE)
213213
}
214+
215+
#' Generate a comprehensive collection test cases for comment / insertion
216+
#' interaction
217+
#' Test consist of if / if-else / if-else-if-else caes, paired with various
218+
#' line-break and comment configurations. Used for internal testing.
219+
#' @return
220+
#' The function is called for its side effects, i.e. to write the
221+
#' test cases to *-in.R files that can be tested with [test_collection()]. Note
222+
#' that a few of the test cases are invalid and need to be removed / commented
223+
#' out manually.
224+
generate_test_samples <- function() {
225+
gen <- function(x) {
226+
if (length(x) == 0) ""
227+
else {
228+
c(
229+
paste0(x[1], gen(x[-1])),
230+
paste0(x[1], " # comment\n", paste(x[-1], collapse = ""))
231+
)
232+
}
233+
}
234+
235+
collapse <- function(x) paste(x, collapse = "\n\n")
236+
237+
cat(
238+
collapse(gen(c("if", "(", "TRUE", ")", "NULL"))),
239+
file = "tests/testthat/insertion_comment_interaction/just_if-in.R"
240+
)
241+
cat(
242+
collapse(gen(c("if", "(", "TRUE", ")", "NULL", " else", " NULL"))),
243+
file = "tests/testthat/insertion_comment_interaction/if_else-in.R"
244+
)
245+
cat(collapse(gen(c(
246+
"if", "(", "TRUE", ")", "NULL", " else", " if", "(", "FALSE", ")", "NULL",
247+
" else", " NULL"))),
248+
file = "tests/testthat/insertion_comment_interaction/if_else_if_else-in.R"
249+
)
250+
}

R/token-create.R

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,29 +112,27 @@ validate_new_pos_ids <- function(new_ids, after) {
112112
#' Wrap an expression in curly braces
113113
#'
114114
#' Adds curly braces to an expression (represented as a parse table) if there
115-
#' are none. Because of the nature of the nested parse table, curly braces only appear
116-
#' with a single expression between them, so `wrap_expr_in_curly()` actually
117-
#' first wraps the expression to wrap in curly braces into a new expression and
118-
#' then adds curly braces around this new expression.
115+
#' are none.
119116
#' @param pd A parse table.
120117
#' @param stretch_out Whether or not to create a line break after the opening
121118
#' curly brace and before the closing curly brace.
122-
wrap_expr_in_curly <- function(pd, stretch_out = FALSE) {
119+
wrap_expr_in_curly <- function(pd, stretch_out = c(FALSE, FALSE)) {
123120
if (is_curly_expr(pd)) return(pd)
124-
if (stretch_out) {
121+
if (stretch_out[1]) {
125122
pd$lag_newlines[1] <- 1L
126123
}
127124

128-
expr <- wrap_expr_in_expr(pd)
129125
opening <- create_tokens(
130126
"'{'", "{",
131-
pos_ids = create_pos_ids(expr, 1, after = FALSE)
127+
pos_ids = create_pos_ids(pd, 1, after = FALSE),
128+
spaces = 1 - as.integer(stretch_out[1])
132129
)
133130

134131
closing <- create_tokens(
135-
"'}'", "}", spaces = 1, lag_newlines = as.integer(stretch_out),
132+
"'}'", "}", spaces = 1, lag_newlines = as.integer(stretch_out[2]),
136133
pos_ids = create_pos_ids(pd, nrow(pd), after = TRUE)
137134
)
138135

139-
bind_rows(opening, expr, closing)
136+
bind_rows(opening, pd, closing) %>%
137+
set_multi_line()
140138
}

R/utils.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@ is_unsaved_file <- function(path) {
6767
#' @param pos The position of the token to start the search from.
6868
#' @importFrom rlang seq2
6969
next_non_comment <- function(pd, pos) {
70+
if (length(pos) < 1 || is.na(pos) || pos >= nrow(pd)) return(integer(0))
7071
candidates <- seq2(pos + 1L, nrow(pd))
72+
if (all(candidates %in% which(pd$token == "COMMENT"))) return(integer(0))
7173
setdiff(candidates, which(pd$token == "COMMENT"))[1]
7274
}
75+
76+
#' Find the index of the last comment in the sequence of comments-only tokens
77+
#' after the token that has position `pos` in `pd`.
78+
#' @param pd A parse table.
79+
#' @param pos The position of the token to start the search from.
80+
extend_if_comment <- function(pd, pos) {
81+
if (pos == nrow(pd)) return(pos)
82+
if (pd$token[pos + 1] == "COMMENT") {
83+
extend_if_comment(pd, pos + 1L)
84+
} else {
85+
pos
86+
}
87+
}

man/contains_else_expr_that_needs_braces.Rd

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/extend_if_comment.Rd

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/generate_test_samples.Rd

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/wrap_expr_in_curly.Rd

Lines changed: 2 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)