diff --git a/DESCRIPTION b/DESCRIPTION index 0462a33..1518a16 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: BiocCheck Title: Bioconductor-specific package checks -Version: 1.49.30 -Date: 2026-07-27 +Version: 1.49.31 +Date: 2026-08-31 Authors@R: c( person("Bioconductor", "Package Maintainer", , "maintainer@bioconductor.org", "aut"), @@ -33,6 +33,7 @@ Imports: commonmark, graph, httr2, + jsonlite, knitr, methods, rvest, @@ -45,7 +46,6 @@ Suggests: curl, devtools, gert, - jsonlite, rmarkdown, tinytest, usethis diff --git a/NAMESPACE b/NAMESPACE index 0228615..b7b9ec2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -34,6 +34,8 @@ importFrom(httr2,request) importFrom(httr2,resp_body_html) importFrom(httr2,resp_body_json) importFrom(httr2,resp_status) +importFrom(jsonlite,read_json) +importFrom(jsonlite,toJSON) importFrom(knitr,purl) importFrom(stringdist,stringdistmatrix) importFrom(tools,Rd2ex) diff --git a/NEWS b/NEWS index b2f51a0..75d2fca 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,12 @@ NEW FEATURES non-standard fields in the `DESCRIPTION` file. o Add check for S4 classes to verify they provide a non-derived default `show()` method. + o Write a machine-readable `00BiocCheck.json` report next to + `00BiocCheck.log` in the `.BiocCheck` folder. It includes the + session metadata, a `summary` count of the errors, warnings, and notes, the + overall `status`, one structured `entries` record per condition raised + (with the originating check function and file locations where reported), + and the plain text report. See the vignette for the schema. BUG FIXES AND MINOR IMPROVEMENTS @@ -28,6 +34,11 @@ BUG FIXES AND MINOR IMPROVEMENTS via `.gitignore` in `BiocCheckGitClone`. o Optimize `getFunctionLengths` and `checkFunctionLengths` using vectorized operations for faster execution + o Coding practice checks now report file paths relative to the package + directory, e.g., `R/foo.R`, rather than the file name alone. + o The `toJSON` and `fromJSON` methods of the `BiocCheck` class no longer + doubly encode the report; `toJSON` returns the JSON when no `file` is + given. o Support and update testing for roxygen2 version 8 o Improve ORCID checker to correctly validate invalid check-sum characters o Resolve path linter false positives and use `file.path` for portable diff --git a/R/BiocCheck-class.R b/R/BiocCheck-class.R index f0cc59f..348fd23 100644 --- a/R/BiocCheck-class.R +++ b/R/BiocCheck-class.R @@ -42,6 +42,13 @@ #' #' @field error,warning,note `list()` Finer extraction of each condition type #' +#' @field entries `list()` A flat list of records, one per +#' condition raised, each with the `severity`, the +#' originating check function (`checkFun`), the check title +#' (`check`), the `message`, any `help_text` and `details`, +#' and the file `locations` when reported by the check. This +#' is the machine-readable form written to `00BiocCheck.json`. +#' #' @field metadata `list()` A list of additional information relevant to the #' package and its state. See details. #' @@ -74,8 +81,15 @@ #' #' @param file `character(1)` A path to a JSON file for writing or reading as #' created by `toJSON` and `fromJSON` `BiocCheck` methods. +#' When `NULL`, `toJSON` returns the JSON as a character +#' string instead of writing it. +#' +#' @param text `character()` The plain text report, as included +#' in the `text` element of the JSON output. Defaults to the +#' output of `composeReport`. #' #' @importFrom BiocBaseUtils checkInstalled +#' @importFrom jsonlite read_json toJSON #' @importFrom utils tail #' #' @section methods: @@ -86,15 +100,18 @@ #' * `setCheck`: Create a new element in the internal list for a check #' * `get`: Extract the list of conditions raised by `BiocCheck` #' * `getNum`: Tally the number of condition provided by the input +#' * `getStatus`: The worst condition raised, i.e., one of `error`, +#' `warning`, `note`, or `ok` when nothing was raised #' * `zero`: Reset the internal log of the condition provided #' * `getBiocCheckDir`: Report and create the `.BiocCheck` #' directory as obtained from the metadata #' * `composeReport`: Simplify the list structure from the `log` and #' provide a character vector of conditions raised -#' * `report`: Write the `00BiocCheck.log` report into the `BiocCheck` -#' folder -#' * `toJSON`: Write a JSON file to the location indicated with the -#' conditions raised +#' * `report`: Write the `00BiocCheck.log` and +#' `00BiocCheck.json` reports into the `BiocCheck` folder +#' * `toJSON`: Write (or return) the machine-readable report: +#' the `metadata`, a `summary` count of each condition, the +#' overall `status`, the `entries`, and the `text` report #' * `fromJSON`: Read a JSON file from the location indicated with the #' output of previous conditions raised in the check #' * `show`: Display the information in the class. Currently empty. @@ -122,6 +139,8 @@ NULL error = "list", warning = "list", note = "list", + # flat, machine-readable record of every condition raised + entries = "list", metadata = "list" ), methods = list( @@ -146,6 +165,10 @@ NULL .messages$setMessage(nist, condition = condition) .self[[condition]] <- append(.self[[condition]], nist) .self$log[[checkName]] <- append(.self$log[[checkName]], nist) + .self$entries <- c( + .self$entries, + list(.entry(mlist, checkName, condition, help_text, messages)) + ) }, addMetadata = function(BiocPackage, ...) { args <- list(...) @@ -203,6 +226,10 @@ NULL for (condition in conditions) { .self[[condition]] <- list() } + .self$entries <- Filter( + function(entry) !entry[["severity"]] %in% conditions, + .self$entries + ) }, getBiocCheckDir = function() { bioccheck_dir <- .self$metadata$BiocCheckDir @@ -227,17 +254,47 @@ NULL writeLines( outputs, con = file.path(bioccheck_dir, "00BiocCheck.log") ) + .self$toJSON( + file = file.path(bioccheck_dir, "00BiocCheck.json"), + text = outputs + ) }, - toJSON = function(file) { - out <- Filter(length, .self$log) - checkInstalled("jsonlite") - jlog <- jsonlite::toJSON(out, auto_unbox = FALSE) - jsonlite::write_json(jlog, file) + getStatus = function() { + counts <- .self$getNum() + worst <- names(counts)[counts > 0L] + if (length(worst)) worst[[1L]] else "ok" + }, + toJSON = function(file = NULL, text = .self$composeReport()) { + payload <- list( + ## an empty list would serialize as '[]' rather than '{}' + metadata = if (length(.self$metadata)) + .self$metadata + else + structure(list(), names = character(0L)), + summary = as.list(.self$getNum()), + status = .self$getStatus(), + entries = .self$entries, + ## some conditions embed newlines; split so that 'text' + ## matches the '00BiocCheck.log' file line for line + text = as.list( + strsplit( + paste(text, collapse = "\n"), "\n", fixed = TRUE + )[[1L]] + ) + ) + json <- jsonlite::toJSON( + payload, auto_unbox = TRUE, pretty = TRUE, null = "null" + ) + if (is.null(file)) + json + else + writeLines(json, con = file) }, fromJSON = function(file) { - checkInstalled("jsonlite") - infile <- jsonlite::read_json(file)[[1]] - .self[["log"]] <- jsonlite::fromJSON(infile, simplifyVector = FALSE) + payload <- jsonlite::read_json(file, simplifyVector = FALSE) + .self$metadata <- payload[["metadata"]] + .self$entries <- payload[["entries"]] + payload }, show = function() { invisible() @@ -254,6 +311,49 @@ NULL ) ) +## The two location formats emitted by the checks, i.e., '.lineReport' and +## sprintf("%s (line %d, column %d)"), the latter optionally prefixed with the +## symbol found, e.g., "sapply() in R/foo.R (line 3, column 5)". Parsed once +## here so that consumers of the JSON report never have to parse them. Chunk +## locations in vignettes are skipped: their lines are relative to the chunk. +.locPatterns <- c( + "^([^[:space:]]+)#L([0-9]+)", + "([^[:space:]]+) \\(line ([0-9]+), column ([0-9]+)\\)$" +) + +.parseLocations <- function(messages) { + messages <- as.character(messages) + for (pattern in .locPatterns) { + hits <- grepl(pattern, messages) + if (!any(hits)) + next + parts <- regmatches(messages[hits], regexec(pattern, messages[hits])) + parts <- do.call(rbind, parts) + res <- data.frame( + file = parts[, 2L], line = as.integer(parts[, 3L]) + ) + if (ncol(parts) > 3L) + res[["column"]] <- as.integer(parts[, 4L]) + return(res) + } + NULL +} + +## one flat, machine-readable record per condition raised. 'checkFun' is the +## stable identifier for downstream tools; 'check' is the human-readable title. +.entry <- function(mlist, checkName, condition, help_text, messages) { + list( + severity = condition, + checkFun = names(mlist), + check = checkName, + message = paste(unlist(mlist, use.names = FALSE), collapse = " "), + help_text = if (length(help_text)) + paste(help_text, collapse = " "), + details = I(as.character(messages)), + locations = .parseLocations(messages) + ) +} + .flattenElement <- function(listElem) { debugFun <- names(listElem) lowerElem <- unlist(listElem, use.names = FALSE) diff --git a/R/BiocCheck.R b/R/BiocCheck.R index e606fc4..bb68144 100644 --- a/R/BiocCheck.R +++ b/R/BiocCheck.R @@ -107,7 +107,9 @@ #' #' @return `BiocCheck()` is chiefly called for the side effect of the check #' reporting. The function also creates a `.BiocCheck` folder -#' and returns a `BiocCheck` reference class with three main list elements: +#' with the `00BiocCheck.log` text report and the `00BiocCheck.json` +#' machine-readable report (see the vignette for the JSON schema), and +#' returns a `BiocCheck` reference class with three main list elements: #' #' * **error**: Items to address before the package can be accepted #' diff --git a/R/checkRcoding.R b/R/checkRcoding.R index 59e3321..10a7cb7 100644 --- a/R/checkRcoding.R +++ b/R/checkRcoding.R @@ -222,7 +222,7 @@ check1toN <- function(.BiocPackage) { tokens <- tokens[ tokens[,"text"] == "1", , drop=FALSE] sprintf( "%s (line %d, column %d)", - basename(rfile), tokens[,"line1"], tokens[,"col1"] + .getDirFiles(rfile), tokens[,"line1"], tokens[,"col1"] ) }) msg_seq <- unlist(msg_seq) @@ -231,7 +231,7 @@ check1toN <- function(.BiocPackage) { checkSingleColon <- function(.BiocPackage, avail_pkgs = character(0L)) { rfiles <- .BiocPackage$RSources - names(rfiles) <- basename(rfiles) + names(rfiles) <- .getDirFiles(rfiles) colon_pres <- lapply(rfiles, function(rfile) { tokens <- getParseData(parse(rfile, keep.source = TRUE)) tokens <- tokens[tokens[,"token"] != "expr", ,drop=FALSE] @@ -502,7 +502,7 @@ getClassNEEQLookup <- function(rfile) { checkClassNEEQLookup <- function(.BiocPackage) { rfiles <- .BiocPackage$RSources - names(rfiles) <- basename(rfiles) + names(rfiles) <- .getDirFiles(rfiles) NEEQ_pres <- lapply(rfiles, getClassNEEQLookup) NEEQ_pres <- Filter(nrow, NEEQ_pres) msg_neeq <- lapply(names(NEEQ_pres), function(rfile, framelist) { @@ -532,7 +532,7 @@ checkExternalData <- function(.BiocPackage) { sprintf( "%s (line %d, column %d)", - basename(rfile), tokens[,"line1"], tokens[,"col1"] + .getDirFiles(rfile), tokens[,"line1"], tokens[,"col1"] ) }) unlist(msg_eda) diff --git a/README.md b/README.md index e5b0f52..30260ef 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,20 @@ BiocManager::install("Bioconductor/BiocAddins") Then, in RStudio, click on the "Addins" menu, and select "Run BiocCheck". +## Machine-readable output + +Each run writes both `00BiocCheck.log` and `00BiocCheck.json` to the +`.BiocCheck` folder. The JSON report contains the text output +along with a `summary` count of the errors, warnings, and notes, an overall +`status`, and one structured record per condition raised, so that continuous +integration jobs and other tools do not have to parse the text output: + +```sh +jq -e '.summary.error == 0' MyPackage.BiocCheck/00BiocCheck.json +``` + +See `vignette("BiocCheck")` for the schema. + ## Documentation The `BiocCheck` package contains a vignette that describes the package diff --git a/inst/tinytest/test_json.R b/inst/tinytest/test_json.R new file mode 100644 index 0000000..62d51f7 --- /dev/null +++ b/inst/tinytest/test_json.R @@ -0,0 +1,168 @@ +source("helpers.R") + +# machine-readable report ------------------------------------------------- +cli::cli_h3("JSON report") + +## conditions are raised from within a check function; 'checkFun' is taken +## from the calling function's name +noteCheck <- function() { + BiocCheck:::handleNote("A note") +} +warnCheck <- function() { + BiocCheck:::handleWarningFiles( + "A warning", messages = "R/foo.R (line 3, column 5)" + ) +} +errorCheck <- function() { + BiocCheck:::handleErrorFiles( + "An error", messages = "R/bar.R#L42 x <- 1 ..." + ) +} + +.BiocCheck$zero() +BiocCheck:::handleCheck("Checking JSON output...") +noteCheck() +warnCheck() +errorCheck() + +json <- .BiocCheck$toJSON() +expect_true(jsonlite::validate(json)) + +payload <- jsonlite::fromJSON(json, simplifyVector = FALSE) +expect_identical( + names(payload), c("metadata", "summary", "status", "entries", "text") +) + +## the summary is the tally of the conditions raised +expect_equal( + unlist(payload[["summary"]]), c(error = 1, warning = 1, note = 1) +) +expect_identical(payload[["status"]], "error") +expect_identical( + length(payload[["entries"]]), sum(.BiocCheck$getNum()) +) + +entries <- payload[["entries"]] +expect_identical( + vapply(entries, `[[`, character(1L), "severity"), + c("note", "warning", "error") +) +expect_identical( + vapply(entries, `[[`, character(1L), "checkFun"), + c("noteCheck", "warnCheck", "errorCheck") +) +expect_identical( + vapply(entries, `[[`, character(1L), "check"), + rep("Checking JSON output...", 3L) +) +expect_identical(entries[[1L]][["message"]], "A note") + +## 'help_text' is absent as 'null', not as an empty array +expect_null(entries[[1L]][["help_text"]]) +expect_identical(entries[[2L]][["help_text"]], "Found in files:") + +## 'details' is always an array, even when a single message is reported +expect_true(is.list(entries[[1L]][["details"]])) +expect_identical(length(entries[[1L]][["details"]]), 0L) +expect_identical( + entries[[3L]][["details"]], list("R/bar.R#L42 x <- 1 ...") +) + +## locations are parsed from both formats emitted by the checks +expect_null(entries[[1L]][["locations"]]) +expect_identical( + entries[[2L]][["locations"]], + list(list(file = "R/foo.R", line = 3L, column = 5L)) +) +expect_identical( + entries[[3L]][["locations"]], list(list(file = "R/bar.R", line = 42L)) +) + +# .parseLocations --------------------------------------------------------- +cli::cli_h3(".parseLocations") + +expect_null(BiocCheck:::.parseLocations(character(0L))) +expect_null(BiocCheck:::.parseLocations("no location here")) +expect_identical( + BiocCheck:::.parseLocations( + c("R/a.R#L1 code ...", "vignettes/b.Rmd#L20 more code ...") + ), + data.frame(file = c("R/a.R", "vignettes/b.Rmd"), line = c(1L, 20L)) +) +expect_identical( + BiocCheck:::.parseLocations( + c("a.R (line 1, column 2)", "b.R (line 30, column 4)") + ), + data.frame( + file = c("a.R", "b.R"), line = c(1L, 30L), column = c(2L, 4L) + ) +) +## the symbol found is not part of the file name +expect_identical( + BiocCheck:::.parseLocations( + c( + "sapply() in R/a.R (line 1, column 2)", + "update.packages() in R/b.R (line 3, column 4)" + ) + ), + data.frame( + file = c("R/a.R", "R/b.R"), line = c(1L, 3L), column = c(2L, 4L) + ) +) +## chunk-relative lines are not reported as file locations +expect_null( + BiocCheck:::.parseLocations("a.Rmd (chunk no. 2, line 1, column 2)") +) + +# round trip -------------------------------------------------------------- +cli::cli_h3("toJSON / fromJSON") + +jsonfile <- tempfile(fileext = ".json") +.BiocCheck$toJSON(file = jsonfile) +expect_true(file.exists(jsonfile)) + +messages <- vapply(.BiocCheck$entries, `[[`, character(1L), "message") +.BiocCheck$zero() +expect_identical(.BiocCheck$entries, list()) + +roundtrip <- .BiocCheck$fromJSON(jsonfile) +expect_identical(roundtrip[["status"]], "error") +expect_identical( + vapply(.BiocCheck$entries, `[[`, character(1L), "message"), messages +) + +## an empty report is still valid and reports an 'ok' status +.BiocCheck$entries <- list() +payload <- jsonlite::fromJSON(.BiocCheck$toJSON(), simplifyVector = FALSE) +expect_identical(payload[["status"]], "ok") +expect_identical(payload[["entries"]], list()) +expect_equal( + unlist(payload[["summary"]]), c(error = 0, warning = 0, note = 0) +) + +# report ------------------------------------------------------------------ +cli::cli_h3("report") + +bioccheck_dir <- file.path(tempfile(), "test.BiocCheck") +.BiocCheck$metadata <- list(Package = "test", BiocCheckDir = bioccheck_dir) +BiocCheck:::handleCheck("Checking report output...") +noteCheck() + +.BiocCheck$report(debug = FALSE, isOnBBS = TRUE) +expect_false(dir.exists(bioccheck_dir)) + +.BiocCheck$report(debug = FALSE, isOnBBS = FALSE) +expect_true(file.exists(file.path(bioccheck_dir, "00BiocCheck.log"))) +expect_true(file.exists(file.path(bioccheck_dir, "00BiocCheck.json"))) + +payload <- jsonlite::read_json(file.path(bioccheck_dir, "00BiocCheck.json")) +expect_identical(payload[["status"]], "note") +expect_identical(payload[["metadata"]][["Package"]], "test") +## the JSON is a superset of the plain text report +expect_identical( + unlist(payload[["text"]]), + readLines(file.path(bioccheck_dir, "00BiocCheck.log")) +) + +unlink(dirname(bioccheck_dir), recursive = TRUE) +.BiocCheck$zero() diff --git a/man/BiocCheck-class.Rd b/man/BiocCheck-class.Rd index b3c2271..33cc501 100644 --- a/man/BiocCheck-class.Rd +++ b/man/BiocCheck-class.Rd @@ -32,7 +32,13 @@ Bioconductor Build System (BBS). This is helpful for avoiding the creation of folders in the BBS.} \item{file}{\code{character(1)} A path to a JSON file for writing or reading as -created by \code{toJSON} and \code{fromJSON} \code{BiocCheck} methods.} +created by \code{toJSON} and \code{fromJSON} \code{BiocCheck} methods. +When \code{NULL}, \code{toJSON} returns the JSON as a character +string instead of writing it.} + +\item{text}{\code{character()} The plain text report, as included +in the \code{text} element of the JSON output. Defaults to the +output of \code{composeReport}.} } \value{ An internal \code{BiocCheck} R5 Reference Class used to document @@ -80,6 +86,13 @@ purposes.} \item{\code{error,warning,note}}{\code{list()} Finer extraction of each condition type} +\item{\code{entries}}{\code{list()} A flat list of records, one per +condition raised, each with the \code{severity}, the +originating check function (\code{checkFun}), the check title +(\code{check}), the \code{message}, any \code{help_text} and \code{details}, +and the file \code{locations} when reported by the check. This +is the machine-readable form written to \verb{00BiocCheck.json}.} + \item{\code{metadata}}{\code{list()} A list of additional information relevant to the package and its state. See details.} }} @@ -94,15 +107,18 @@ package and its state. See details.} \item \code{setCheck}: Create a new element in the internal list for a check \item \code{get}: Extract the list of conditions raised by \code{BiocCheck} \item \code{getNum}: Tally the number of condition provided by the input +\item \code{getStatus}: The worst condition raised, i.e., one of \code{error}, +\code{warning}, \code{note}, or \code{ok} when nothing was raised \item \code{zero}: Reset the internal log of the condition provided \item \code{getBiocCheckDir}: Report and create the \verb{.BiocCheck} directory as obtained from the metadata \item \code{composeReport}: Simplify the list structure from the \code{log} and provide a character vector of conditions raised -\item \code{report}: Write the \verb{00BiocCheck.log} report into the \code{BiocCheck} -folder -\item \code{toJSON}: Write a JSON file to the location indicated with the -conditions raised +\item \code{report}: Write the \verb{00BiocCheck.log} and +\verb{00BiocCheck.json} reports into the \code{BiocCheck} folder +\item \code{toJSON}: Write (or return) the machine-readable report: +the \code{metadata}, a \code{summary} count of each condition, the +overall \code{status}, the \code{entries}, and the \code{text} report \item \code{fromJSON}: Read a JSON file from the location indicated with the output of previous conditions raised in the check \item \code{show}: Display the information in the class. Currently empty. diff --git a/man/BiocCheck.Rd b/man/BiocCheck.Rd index 0ba1a86..2a17959 100644 --- a/man/BiocCheck.Rd +++ b/man/BiocCheck.Rd @@ -36,7 +36,9 @@ relevant to developers and contributors to \code{BiocCheck}.} \value{ \code{BiocCheck()} is chiefly called for the side effect of the check reporting. The function also creates a \verb{.BiocCheck} folder -and returns a \code{BiocCheck} reference class with three main list elements: +with the \verb{00BiocCheck.log} text report and the \verb{00BiocCheck.json} +machine-readable report (see the vignette for the JSON schema), and +returns a \code{BiocCheck} reference class with three main list elements: \itemize{ \item \strong{error}: Items to address before the package can be accepted \item \strong{warning}: Strongly suggested items that may require attention diff --git a/vignettes/BiocCheck.Rmd b/vignettes/BiocCheck.Rmd index aec73cb..3037545 100644 --- a/vignettes/BiocCheck.Rmd +++ b/vignettes/BiocCheck.Rmd @@ -50,6 +50,64 @@ BiocCheck("") Note that the `--new-package` option is turned on in the Single Package Builder (SPB) during the new package submission process. +# Machine-readable output + +Alongside the `00BiocCheck.log` text report, `BiocCheck` writes a +`00BiocCheck.json` file to the `.BiocCheck` folder. It contains +everything in the text report plus structured fields, so that continuous +integration jobs, editors, and other tools can act on the results without +parsing the text output: + +```json +{ + "metadata": { "Package": "MyPackage", "PackageVersion": "0.99.0", + "BiocVersion": "3.23", "...": "" }, + "summary": { "error": 1, "warning": 2, "note": 5 }, + "status": "error", + "entries": [ + { + "severity": "warning", + "checkFun": "checkFormatting", + "check": "Checking formatting of DESCRIPTION, NAMESPACE, ...", + "message": "Consider shorter lines; 3 lines (1%) are > 80 chars.", + "help_text": "First few lines:", + "details": ["R/foo.R#L12 x <- some_very_long_call( ..."], + "locations": [ { "file": "R/foo.R", "line": 12 } ] + } + ], + "text": ["* Checking formatting of DESCRIPTION, NAMESPACE, ...", "..."] +} +``` + +* `summary` tallies the conditions raised and `status` is the most severe of + them, i.e., one of `error`, `warning`, `note`, or `ok`. +* `entries` has one record per condition raised. `checkFun` is the name of the + function that raised it and is the stable identifier to key on; `check` is + the human-readable title, which may be reworded between releases. +* `help_text` and `locations` are `null` when the check does not report them. + `locations` are file paths relative to the package directory; the `column` + element is present only for checks that report one. +* `text` is the `00BiocCheck.log` report, line by line. + +The same information is available from the object returned by `BiocCheck()`, +without reading the file: + +```{r json, eval = FALSE} +bc <- BiocCheck("") +bc$getStatus() +bc$getNum() +bc$entries[[1]] +jsonlite::fromJSON(bc$toJSON()) +``` + +To fail a job when a package raises errors, either use the JSON summary or the +`quit-with-status` option, which exits with a non-zero status when errors are +present: + +```sh +jq -e '.summary.error == 0' MyPackage.BiocCheck/00BiocCheck.json +``` + # When should `BiocCheck` be run `BiocCheck` should always be run after `R CMD check`. @@ -634,7 +692,8 @@ The output of this check includes the first few lines of offending code for many of the various checks. To see the full output, users are encouraged to run the check locally and browse to the `BiocCheck` output folder that was created with the `.BiocCheck` naming convention. The full output -log file is named `00BiocCheck.log`. +log file is named `00BiocCheck.log` and its machine-readable counterpart +`00BiocCheck.json`. There are several helpful packages that can be used for formatting of R code to particular coding standards such as [formatR][formatR] and