From 810ca123eec1a91a6e9ea7fecbe16cb5f1cd4e36 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 24 Jun 2024 17:42:44 +0100 Subject: [PATCH 1/8] Always strip trailing `\n` I can't tell what the original logic was supposed to be, and it seemed inconsistent. Given that the newline was sometimes present and sometimes absent, I assume downstream code must already deal with the differences so this change won't break anything, but we'll need to double-check that. I have confirmed that all testthat tests still pass and it doesn't affect any snapshot output. --- NEWS.md | 1 + R/parse.R | 61 +++++++++++++++---------------------- man/parse_all.Rd | 15 +++++---- tests/testthat/test-eval.R | 6 ---- tests/testthat/test-parse.R | 20 +++++++++++- 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/NEWS.md b/NEWS.md index eb636ef5..674b32c0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # evaluate (development version) +* `parse_all()` consistently strips the trailing `\n` from the end of each `src` vector. * The package now depends on R 4.0.0 in order to decrease our maintenance burden. * `evaluate()` automatically strips calls from conditions emitted by top-level code (these incorrectly get calls because they're wrapped inside `eval()`) (#150). * `evalute(include_timing)` has been deprecated. I can't find any use of it on GitHub, and it adds substantial code complexity for little gain. diff --git a/R/parse.R b/R/parse.R index f57cb868..43a2d34b 100644 --- a/R/parse.R +++ b/R/parse.R @@ -11,12 +11,15 @@ #' A data frame with columns `src`, a character vector of source code, and #' `expr`, a list-column of parsed expressions. There will be one row for each #' top-level expression in `x`. A top-level expression is a complete expression -#' which would trigger execution if typed at the console. The `expression` -#' object in `expr` can be of any length: it will be 0 if the top-level -#' expression contains only whitespace and/or comments; 1 if the top-level -#' expression is a single scalar (like `TRUE`, `1`, or `"x"`), name, or call; -#' or 2 if the top-level expression uses `;` to put multiple expressions on -#' one line. +#' which would trigger execution if typed at the console. +#' +#' The trailing `\n` at the end of each `src` is implicit.` +#' +#' The `expression` object in `expr` can be of any length: it will be 0 if +#' the top-level expression contains only whitespace and/or comments; 1 if +#' the top-level expression is a single scalar (like `TRUE`, `1`, or `"x"`), +#' name, or call; or 2 if the top-level expression uses `;` to put multiple +#' expressions on one line. #' #' If there are syntax errors in `x` and `allow_error = TRUE`, the data #' frame will have an attribute `PARSE_ERROR` that stores the error object. @@ -35,23 +38,26 @@ parse_all <- function(x, filename = NULL, allow_error = FALSE) UseMethod("parse_ #' @export parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { - if (length(grep("\n", x))) { - # strsplit('a\n', '\n') needs to return c('a', '') instead of c('a') - x <- gsub("\n$", "\n\n", x) - x[x == ""] <- "\n" + if (any(grepl("\n", x))) { + # Standardise to character vector with one line per element: + # this is the input that parse() is documented to accept x <- unlist(strsplit(x, "\n"), recursive = FALSE, use.names = FALSE) } n <- length(x) - if (is.null(filename)) - filename <- "" + filename <- filename %||% "" src <- srcfilecopy(filename, x) if (allow_error) { exprs <- tryCatch(parse(text = x, srcfile = src), error = identity) - if (inherits(exprs, 'error')) return(structure( - data.frame(src = paste(x, collapse = '\n'), expr = I(list(expression()))), - PARSE_ERROR = exprs - )) + if (inherits(exprs, 'error')) { + return(structure( + data.frame( + src = paste(x, collapse = '\n'), + expr = I(list(expression())) + ), + PARSE_ERROR = exprs + )) + } } else { exprs <- parse(text = x, srcfile = src) } @@ -59,12 +65,12 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { # No code, only comments and/or empty lines ne <- length(exprs) if (ne == 0) { - return(data.frame(src = append_break(x), expr = I(rep(list(expression()), n)))) + return(data.frame(src = x, expr = I(rep(list(expression()), n)))) } srcref <- attr(exprs, "srcref", exact = TRUE) - # Stard/End line numbers of expressions + # Start/Ene line numbers of expressions pos <- do.call(rbind, lapply(srcref, unclass))[, c(7, 8), drop = FALSE] l1 <- pos[, 1] l2 <- pos[, 2] @@ -102,32 +108,13 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { ) })) - # Bind everything into a data frame, order it by line numbers, append \n to - # all src lines except the last one, and remove the line numbers res <- do.call(rbind, res) res <- res[order(res$line), ] - res$src <- append_break(res$src) res$line <- NULL - - # For compatibility with evaluate (<= 0.5.7): remove the last empty line (YX: - # I think this is a bug) - n <- nrow(res) - if (res$src[n] == "") res <- res[-n, ] - rownames(res) <- NULL res } -# YX: It seems evaluate (<= 0.5.7) had difficulties with preserving line breaks, -# so it ended up with adding \n to the first n-1 lines, which does not seem to -# be necessary to me, and is actually buggy. I'm not sure if it is worth shaking -# the earth and work with authors of reverse dependencies to sort this out. Also -# see #42. -append_break <- function(x) { - n <- length(x) - if (n <= 1) x else paste(x, rep(c("\n", ""), c(n - 1, 1)), sep = "") -} - #' @export parse_all.connection <- function(x, filename = NULL, ...) { if (!isOpen(x, "r")) { diff --git a/man/parse_all.Rd b/man/parse_all.Rd index 26eee692..f747cfb5 100644 --- a/man/parse_all.Rd +++ b/man/parse_all.Rd @@ -18,12 +18,15 @@ If a connection, will be opened and closed only if it was closed initially.} A data frame with columns \code{src}, a character vector of source code, and \code{expr}, a list-column of parsed expressions. There will be one row for each top-level expression in \code{x}. A top-level expression is a complete expression -which would trigger execution if typed at the console. The \code{expression} -object in \code{expr} can be of any length: it will be 0 if the top-level -expression contains only whitespace and/or comments; 1 if the top-level -expression is a single scalar (like \code{TRUE}, \code{1}, or \code{"x"}), name, or call; -or 2 if the top-level expression uses \verb{;} to put multiple expressions on -one line. +which would trigger execution if typed at the console. + +The trailing \verb{\\n} at the end of each \code{src} is implicit.` + +The \code{expression} object in \code{expr} can be of any length: it will be 0 if +the top-level expression contains only whitespace and/or comments; 1 if +the top-level expression is a single scalar (like \code{TRUE}, \code{1}, or \code{"x"}), +name, or call; or 2 if the top-level expression uses \verb{;} to put multiple +expressions on one line. If there are syntax errors in \code{x} and \code{allow_error = TRUE}, the data frame will have an attribute \code{PARSE_ERROR} that stores the error object. diff --git a/tests/testthat/test-eval.R b/tests/testthat/test-eval.R index f6553e01..3424625b 100644 --- a/tests/testthat/test-eval.R +++ b/tests/testthat/test-eval.R @@ -79,12 +79,6 @@ test_that("multiple expressions on one line can get printed as expected", { expect_output_types(ev, c("source", "text", "text")) }) -test_that("multiple lines of comments do not lose the terminating \\n", { - ev <- evaluate("# foo\n#bar") - expect_output_types(ev, c("source", "source")) - expect_equal(ev[[1]]$src, "# foo\n") -}) - test_that("check_stop_on_error converts integer to enum", { expect_equal(check_stop_on_error(0), "continue") expect_equal(check_stop_on_error(1), "stop") diff --git a/tests/testthat/test-parse.R b/tests/testthat/test-parse.R index 2c592125..bbf1189e 100644 --- a/tests/testthat/test-parse.R +++ b/tests/testthat/test-parse.R @@ -1,3 +1,21 @@ +test_that("every TLE has an implicit nl", { + expect_equal(parse_all("x")$src, "x") + expect_equal(parse_all("x\n")$src, "x") + expect_equal(parse_all("")$src, "") + expect_equal(parse_all("\n")$src, "") + + expect_equal(parse_all("{\n1\n}")$src, "{\n1\n}") + expect_equal(parse_all("{\n1\n}\n")$src, "{\n1\n}") + + # even empty lines + expect_equal(parse_all("a\n\nb")$src, c("a", "", "b")) + expect_equal(parse_all("\n\n")$src, c("", "")) +}) + +test_that("a character vector is equivalent to a multi-line string", { + expect_equal(parse_all(c("a", "b")), parse_all(c("a\nb"))) +}) + test_that("expr is always an expression", { expect_equal(parse_all("#")$expr[[1]], expression()) expect_equal(parse_all("1")$expr[[1]], expression(1), ignore_attr = "srcref") @@ -34,7 +52,7 @@ if (isTRUE(l10n_info()[['UTF-8']])) { test_that("multibyte characters are parsed correct", { code <- c("ϱ <- 1# g / ml", "äöüßÄÖÜπ <- 7 + 3# nonsense") - expect_identical(parse_all(code)$src, append_break(code)) + expect_identical(parse_all(code)$src, code) }) } From c940b43a7298bf02c3397fb797e7242c36daabfb Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 24 Jun 2024 17:43:33 +0100 Subject: [PATCH 2/8] Fix typo --- R/parse.R | 2 +- man/parse_all.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/parse.R b/R/parse.R index 43a2d34b..e3728fd5 100644 --- a/R/parse.R +++ b/R/parse.R @@ -13,7 +13,7 @@ #' top-level expression in `x`. A top-level expression is a complete expression #' which would trigger execution if typed at the console. #' -#' The trailing `\n` at the end of each `src` is implicit.` +#' The trailing `\n` at the end of each `src` is implicit. #' #' The `expression` object in `expr` can be of any length: it will be 0 if #' the top-level expression contains only whitespace and/or comments; 1 if diff --git a/man/parse_all.Rd b/man/parse_all.Rd index f747cfb5..13be6999 100644 --- a/man/parse_all.Rd +++ b/man/parse_all.Rd @@ -20,7 +20,7 @@ A data frame with columns \code{src}, a character vector of source code, and top-level expression in \code{x}. A top-level expression is a complete expression which would trigger execution if typed at the console. -The trailing \verb{\\n} at the end of each \code{src} is implicit.` +The trailing \verb{\\n} at the end of each \code{src} is implicit. The \code{expression} object in \code{expr} can be of any length: it will be 0 if the top-level expression contains only whitespace and/or comments; 1 if From f593f1dd896e7a2cfa350d67b2a34e9e3caaee17 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 24 Jun 2024 17:46:03 +0100 Subject: [PATCH 3/8] Fix typo --- R/parse.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/parse.R b/R/parse.R index e3728fd5..24f9b3d9 100644 --- a/R/parse.R +++ b/R/parse.R @@ -70,7 +70,7 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { srcref <- attr(exprs, "srcref", exact = TRUE) - # Start/Ene line numbers of expressions + # Start/end line numbers of expressions pos <- do.call(rbind, lapply(srcref, unclass))[, c(7, 8), drop = FALSE] l1 <- pos[, 1] l2 <- pos[, 2] From e12f006967db438d7d98e557fca91381ea503758 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 24 Jun 2024 17:55:48 +0100 Subject: [PATCH 4/8] Simpler way to create `pos` --- R/parse.R | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/R/parse.R b/R/parse.R index 24f9b3d9..f738bf85 100644 --- a/R/parse.R +++ b/R/parse.R @@ -69,24 +69,21 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { } srcref <- attr(exprs, "srcref", exact = TRUE) - - # Start/end line numbers of expressions - pos <- do.call(rbind, lapply(srcref, unclass))[, c(7, 8), drop = FALSE] - l1 <- pos[, 1] - l2 <- pos[, 2] - # Add a third column i to store the indices of expressions - pos <- cbind(pos, i = seq_len(nrow(pos))) - pos <- as.data.frame(pos) # split() does not work on matrices + pos <- data.frame( + start = vapply(srcref, `[[`, 7, FUN.VALUE = integer(1)), + end = vapply(srcref, `[`, 8, FUN.VALUE = integer(1)), + i = seq_along(srcref) + ) # Split line number pairs into groups: if the next start line is the same as # the last end line, the two expressions must belong to the same group - spl <- cumsum(c(TRUE, l1[-1] != l2[-ne])) + spl <- cumsum(c(TRUE, pos$start[-1] != pos$end[-ne])) # Extract src lines and expressions for each group; also record the start line # number of this group so we can re-order src/expr later res <- lapply(split(pos, spl), function(p) { n <- nrow(p) data.frame( - src = paste(x[p[1, 1]:p[n, 2]], collapse = "\n"), + src = paste(x[p$start[1]:p$end[n]], collapse = "\n", recycle0 = TRUE), expr = I(list(exprs[p[, 3]])), line = p[1, 1] ) @@ -94,7 +91,7 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { # Now process empty expressions (comments/blank lines); see if there is a # "gap" between the last end number + 1 and the next start number - 1 - pos <- cbind(c(1, l2 + 1), c(l1 - 1, n)) + pos <- cbind(c(1, pos$end + 1), c(pos$start - 1, n)) pos <- pos[pos[, 1] <= pos[, 2], , drop = FALSE] # Extract src lines from the gaps, and assign empty expressions to them From ce97fed493f8ba70cfbda8a3c39a900878da7c5f Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 24 Jun 2024 18:08:48 +0100 Subject: [PATCH 5/8] More clarification --- R/parse.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/parse.R b/R/parse.R index f738bf85..91fb5e3b 100644 --- a/R/parse.R +++ b/R/parse.R @@ -83,9 +83,9 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { res <- lapply(split(pos, spl), function(p) { n <- nrow(p) data.frame( - src = paste(x[p$start[1]:p$end[n]], collapse = "\n", recycle0 = TRUE), - expr = I(list(exprs[p[, 3]])), - line = p[1, 1] + src = paste(x[p$start[1]:p$end[n]], collapse = "\n"), + expr = I(list(exprs[p$i])), + line = p$start[1] ) }) From 9d5cbf7dd5ec9690e6faacc4bf90a40a909dd0c8 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Tue, 25 Jun 2024 08:33:23 +0100 Subject: [PATCH 6/8] Be consistent --- R/parse.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/parse.R b/R/parse.R index 91fb5e3b..7ce6ea8f 100644 --- a/R/parse.R +++ b/R/parse.R @@ -71,7 +71,7 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { srcref <- attr(exprs, "srcref", exact = TRUE) pos <- data.frame( start = vapply(srcref, `[[`, 7, FUN.VALUE = integer(1)), - end = vapply(srcref, `[`, 8, FUN.VALUE = integer(1)), + end = vapply(srcref, `[[`, 8, FUN.VALUE = integer(1)), i = seq_along(srcref) ) From 0fa6d6b735a9242fd2c8288f111d87c390087673 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Tue, 25 Jun 2024 08:46:08 +0100 Subject: [PATCH 7/8] More refactoring --- R/parse.R | 25 ++++++++++--------------- tests/testthat/test-parse.R | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/R/parse.R b/R/parse.R index 7ce6ea8f..48f46942 100644 --- a/R/parse.R +++ b/R/parse.R @@ -71,31 +71,26 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { srcref <- attr(exprs, "srcref", exact = TRUE) pos <- data.frame( start = vapply(srcref, `[[`, 7, FUN.VALUE = integer(1)), - end = vapply(srcref, `[[`, 8, FUN.VALUE = integer(1)), - i = seq_along(srcref) + end = vapply(srcref, `[[`, 8, FUN.VALUE = integer(1)) ) + pos$exprs <- exprs - # Split line number pairs into groups: if the next start line is the same as - # the last end line, the two expressions must belong to the same group + # parse() splits TLEs that use ; into multiple expressions so join back + # together if an expression overlaps on the same line. spl <- cumsum(c(TRUE, pos$start[-1] != pos$end[-ne])) - # Extract src lines and expressions for each group; also record the start line - # number of this group so we can re-order src/expr later - res <- lapply(split(pos, spl), function(p) { + tles <- lapply(split(pos, spl), function(p) { n <- nrow(p) data.frame( src = paste(x[p$start[1]:p$end[n]], collapse = "\n"), - expr = I(list(exprs[p$i])), + expr = I(list(p$exprs)), line = p$start[1] ) }) - # Now process empty expressions (comments/blank lines); see if there is a - # "gap" between the last end number + 1 and the next start number - 1 + # parse() also drops comments and whitespace so we add them back in pos <- cbind(c(1, pos$end + 1), c(pos$start - 1, n)) pos <- pos[pos[, 1] <= pos[, 2], , drop = FALSE] - - # Extract src lines from the gaps, and assign empty expressions to them - res <- c(res, lapply(seq_len(nrow(pos)), function(i) { + comments <- lapply(seq_len(nrow(pos)), function(i) { p <- pos[i, ] r <- p[1]:p[2] data.frame( @@ -103,9 +98,9 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { expr = I(rep(list(expression()), p[2] - p[1] + 1)), line = r - 1 ) - })) + }) - res <- do.call(rbind, res) + res <- do.call(rbind, c(tles, comments)) res <- res[order(res$line), ] res$line <- NULL rownames(res) <- NULL diff --git a/tests/testthat/test-parse.R b/tests/testthat/test-parse.R index bbf1189e..e6f695ec 100644 --- a/tests/testthat/test-parse.R +++ b/tests/testthat/test-parse.R @@ -16,6 +16,24 @@ test_that("a character vector is equivalent to a multi-line string", { expect_equal(parse_all(c("a", "b")), parse_all(c("a\nb"))) }) +test_that("recombines multi-expression TLEs", { + expect_equal( + parse_all("1;2;3")$expr[[1]], + expression(1, 2, 3), + ignore_attr = "srcref" + ) + expect_equal( + parse_all("1+\n2;3")$expr[[1]], + expression(1 + 2, 3), + ignore_attr = "srcref" + ) + expect_equal( + parse_all("1+\n2;3+\n4; 5")$expr[[1]], + expression(1 + 2, 3 + 4, 5), + ignore_attr = "srcref" + ) +}) + test_that("expr is always an expression", { expect_equal(parse_all("#")$expr[[1]], expression()) expect_equal(parse_all("1")$expr[[1]], expression(1), ignore_attr = "srcref") From 765c539a23133b0581f659530b5d0d459a2b3366 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Tue, 25 Jun 2024 09:10:11 +0100 Subject: [PATCH 8/8] Vectorise comment restoration & eliminate special case for 0 exprs --- R/parse.R | 65 ++++++++++++++++++++----------------- tests/testthat/test-parse.R | 11 +++++++ 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/R/parse.R b/R/parse.R index 48f46942..3e91d641 100644 --- a/R/parse.R +++ b/R/parse.R @@ -51,10 +51,7 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { exprs <- tryCatch(parse(text = x, srcfile = src), error = identity) if (inherits(exprs, 'error')) { return(structure( - data.frame( - src = paste(x, collapse = '\n'), - expr = I(list(expression())) - ), + data.frame(src = paste(x, collapse = '\n'), expr = empty_expr()), PARSE_ERROR = exprs )) } @@ -62,12 +59,6 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { exprs <- parse(text = x, srcfile = src) } - # No code, only comments and/or empty lines - ne <- length(exprs) - if (ne == 0) { - return(data.frame(src = x, expr = I(rep(list(expression()), n)))) - } - srcref <- attr(exprs, "srcref", exact = TRUE) pos <- data.frame( start = vapply(srcref, `[[`, 7, FUN.VALUE = integer(1)), @@ -75,10 +66,10 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { ) pos$exprs <- exprs - # parse() splits TLEs that use ; into multiple expressions so join back - # together if an expression overlaps on the same line. - spl <- cumsum(c(TRUE, pos$start[-1] != pos$end[-ne])) - tles <- lapply(split(pos, spl), function(p) { + # parse() splits TLEs that use ; into multiple expressions so we + # join together expressions that overlaps on the same line(s) + line_group <- cumsum(is_new_line(pos$start, pos$end)) + tles <- lapply(split(pos, line_group), function(p) { n <- nrow(p) data.frame( src = paste(x[p$start[1]:p$end[n]], collapse = "\n"), @@ -86,23 +77,21 @@ parse_all.character <- function(x, filename = NULL, allow_error = FALSE) { line = p$start[1] ) }) + tles <- do.call(rbind, tles) - # parse() also drops comments and whitespace so we add them back in - pos <- cbind(c(1, pos$end + 1), c(pos$start - 1, n)) - pos <- pos[pos[, 1] <= pos[, 2], , drop = FALSE] - comments <- lapply(seq_len(nrow(pos)), function(i) { - p <- pos[i, ] - r <- p[1]:p[2] - data.frame( - src = x[r], - expr = I(rep(list(expression()), p[2] - p[1] + 1)), - line = r - 1 - ) - }) - - res <- do.call(rbind, c(tles, comments)) - res <- res[order(res$line), ] - res$line <- NULL + # parse() drops comments and whitespace so we add them back in + gaps <- data.frame(start = c(1, pos$end + 1), end = c(pos$start - 1, n)) + gaps <- gaps[gaps$start <= gaps$end, ,] + # in sequence(), nvec is equivalent to length.out + lines <- sequence(from = gaps$start, nvec = gaps$end - gaps$start + 1) + comments <- data.frame( + src = x[lines], + expr = empty_expr(length(lines)), + line = lines + ) + + res <- rbind(tles, comments) + res <- res[order(res$line), c("src", "expr")] rownames(res) <- NULL res } @@ -164,3 +153,19 @@ parse_all.call <- function(x, filename = NULL, ...) { out$expr <- list(as.expression(x)) out } + +# Helpers --------------------------------------------------------------------- + +empty_expr <- function(n = 1) { + I(rep(list(expression()), n)) +} + +is_new_line <- function(start, end) { + if (length(start) == 0) { + logical() + } else if (length(start) == 1) { + TRUE + } else { + c(TRUE, start[-1] != end[-length(end)]) + } +} diff --git a/tests/testthat/test-parse.R b/tests/testthat/test-parse.R index e6f695ec..c7e1c093 100644 --- a/tests/testthat/test-parse.R +++ b/tests/testthat/test-parse.R @@ -1,3 +1,9 @@ +test_that("can parse even if no expressions", { + expect_equal(parse_all("")$src, "") + expect_equal(parse_all("#")$src, "#") + expect_equal(parse_all("#\n\n")$src, c("#", "")) +}) + test_that("every TLE has an implicit nl", { expect_equal(parse_all("x")$src, "x") expect_equal(parse_all("x\n")$src, "x") @@ -34,6 +40,11 @@ test_that("recombines multi-expression TLEs", { ) }) +test_that("re-integrates lines without expressions", { + expect_equal(parse_all("1\n\n2")$src, c("1", "", "2")) + expect_equal(parse_all("1\n#\n2")$src, c("1", "#", "2")) +}) + test_that("expr is always an expression", { expect_equal(parse_all("#")$expr[[1]], expression()) expect_equal(parse_all("1")$expr[[1]], expression(1), ignore_attr = "srcref")