From 87254086b81c12451ccbfb36c36653e259a1c54d Mon Sep 17 00:00:00 2001 From: Pedro Faria <69123925+pedropark99@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:57:48 -0300 Subject: [PATCH] close #2226: add an error handler to improve YAML error message (#2294) Co-authored-by: Yihui Xie --- NEWS.md | 4 ++++ R/concordance.R | 6 +++--- R/output.R | 23 +++++++++++++++-------- R/parser.R | 19 +++++++++++++++++-- R/utils.R | 4 +--- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/NEWS.md b/NEWS.md index 3c73cd69d5..ad103bdeff 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # CHANGES IN knitr VERSION 1.45 +## NEW FEATURES + +- Improved the error message to contain more specific information when YAML chunk options could not be parsed (thanks, @pedropark99, #2294). + ## BUG FIXES - Special characters in the chunk option `fig.alt` are properly escaped now (thanks, @jay-sf, #2290). diff --git a/R/concordance.R b/R/concordance.R index 6c7c6e6546..3e267cf1e0 100644 --- a/R/concordance.R +++ b/R/concordance.R @@ -2,7 +2,7 @@ # record input/output lines numbers in Rnw/tex and filenames knit_concord = new_defaults(list( - inlines = NULL, outlines = NULL, infile = NULL, outfile = NULL + inlines = NULL, outlines = NULL, infile = NULL, outfile = NULL, block = NULL )) # do not consider child mode for concordance @@ -10,11 +10,11 @@ concord_mode = function() { opts_knit$get('concordance') && !child_mode() } -current_lines = function(i) { +current_lines = function(i = knit_concord$get('block')) { # a helpr function to return line numbers for block i n = knit_concord$get('inlines') n1 = sum(head(n, i)); n0 = n1 - n[i] + 2 - c(min(n0, n1), n1) + paste(c(min(n0, n1), n1), collapse = '-') } # generate concordance for RStudio diff --git a/R/output.R b/R/output.R index a0f6bb625a..1c7b5ca167 100644 --- a/R/output.R +++ b/R/output.R @@ -311,20 +311,18 @@ process_file = function(text, output) { } if (progress && is.function(pb$update)) pb$update(i) group = groups[[i]] - res[i] = withCallingHandlers( + knit_concord$set(block = i) + res[i] = handle_error( withCallingHandlers( if (tangle) process_tangle(group) else process_group(group), error = function(e) if (xfun::pkg_available('rlang', '1.0.0')) rlang::entrace(e) ), - error = function(e) { + function(e, loc) { setwd(wd) write_utf8(res, output %n% stdout()) - message( - '\nQuitting from lines ', paste(current_lines(i), collapse = '-'), - if (labels[i] != '') sprintf(' [%s]', labels[i]), - sprintf(' (%s)', knit_concord$get('infile')) - ) - } + paste0('\nQuitting from lines ', loc) + }, + if (labels[i] != '') sprintf(' [%s]', labels[i]) ) } @@ -337,6 +335,15 @@ process_file = function(text, output) { res } +# if an expr throws an an error, message the location of the error if possible +handle_error = function(expr, handler, label = '') { + withCallingHandlers(expr, error = function(e) { + # return a string to point out the error location + loc = paste0(current_lines(), label, sprintf(' (%s)', knit_concord$get('infile'))) + message(one_string(handler(e, loc))) + }) +} + auto_out_name = function(input, ext = tolower(file_ext(input))) { base = sans_ext(input) name = if (opts_knit$get('tangle')) c(base, '.R') else diff --git a/R/parser.R b/R/parser.R index 78a97ed540..dad04ba981 100644 --- a/R/parser.R +++ b/R/parser.R @@ -20,7 +20,9 @@ split_file = function(lines, set.preamble = TRUE, patterns = knit_patterns$get() knit_concord$set(inlines = sapply(groups, length)) # input line numbers for concordance # parse 'em all - lapply(groups, function(g) { + lapply(seq_along(groups), function(i) { + knit_concord$set(block = i) + g = groups[[i]] block = grepl(chunk.begin, g[1]) if (!set.preamble && !parent_mode()) { return(if (block) '' else g) # only need to remove chunks to get pure preamble @@ -302,7 +304,20 @@ partition_chunk = function(engine, code) { meta = substr(src, nchar(s1) + 1, nchar(src) - nchar(s2)) # see if the metadata looks like YAML or CSV if (grepl('^[^ :]+:($|\\s)', meta[1])) { - meta = yaml::yaml.load(meta, handlers = list(expr = parse_only)) + meta = handle_error( + yaml::yaml.load(meta, handlers = list(expr = parse_only)), + function(e, loc) { + x = e$message + r = 'line (\\d+), column (\\d+)' + m = regmatches(x, regexec(r, x, perl = TRUE))[[1]] + if (length(m) < 3) return() + m = as.integer(m[-1]) # c(row, col) + c( + sprintf('Failed to parse YAML inside code chunk at lines %s:', loc), '', + append(meta, paste0(strrep(' ', m[2]), '^~~~~~'), m[1]), '' + ) + } + ) if (!is.list(meta) || length(names(meta)) == 0) { warning('Invalid YAML option format in chunk: \n', one_string(meta), '\n') meta = list() diff --git a/R/utils.R b/R/utils.R index d32f63b4d1..6025b73a7f 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1141,9 +1141,7 @@ str_split = function(x, split, ...) { # methods to update/close it txt_pb = function(total, labels) { if (getOption('knitr.progress.linenums', FALSE)) { - nums = sapply(seq_along(labels), function(i) { - paste(current_lines(i), collapse = '-') - }) + nums = sapply(seq_along(labels), current_lines) labels = sprintf( '%s%s%s:%s', labels, ifelse(labels == '', '', ' @ '), knit_concord$get('infile'), nums