diff --git a/.lintr b/.lintr index d7eb75657..8a6cdf169 100644 --- a/.lintr +++ b/.lintr @@ -15,7 +15,6 @@ linters: linters_with_tags( implicit_assignment_linter = NULL, # false positives with data.table := strings_as_factors_linter = NULL, # R 4.0+ defaults to stringsAsFactors = FALSE one_call_pipe_linter = NULL, # stylistic preference - keyword_quote_linter = NULL, # stylistic preference unnecessary_concatenation_linter = NULL, # intentional patterns library_call_linter = NULL, # intentional patterns in scripts pipe_consistency_linter = NULL # one_call_pipe_linter exclusion triggers false positives diff --git a/R/class-forecast-binary.R b/R/class-forecast-binary.R index bf4d0b835..48d8a771e 100644 --- a/R/class-forecast-binary.R +++ b/R/class-forecast-binary.R @@ -69,12 +69,11 @@ assert_forecast.forecast_binary <- function( forecast, c("sample_id", "quantile_level") ) if (!columns_correct) { - #nolint start: keyword_quote_linter cli_abort( c( - "!" = "Checking `forecast`: Input looks like a binary forecast, but an + `!` = "Checking `forecast`: Input looks like a binary forecast, but an additional column called `sample_id` or `quantile` was found.", - "i" = "Please remove the column." + `i` = "Please remove the column." ) ) } @@ -82,11 +81,10 @@ assert_forecast.forecast_binary <- function( if (!isTRUE(input_check)) { cli_abort( c( - "!" = "Checking `forecast`: Input looks like a binary forecast, but + `!` = "Checking `forecast`: Input looks like a binary forecast, but found the following issue: {input_check}" ) ) - #nolint end } return(invisible(NULL)) } diff --git a/R/class-forecast-nominal.R b/R/class-forecast-nominal.R index 217b25559..b8ef34b86 100644 --- a/R/class-forecast-nominal.R +++ b/R/class-forecast-nominal.R @@ -100,7 +100,7 @@ assert_forecast.forecast_nominal <- function( if (!all(complete$correct)) { first_issue <- complete[(correct), ..forecast_unit][1] first_issue <- lapply(first_issue, FUN = as.character) - #nolint start: keyword_quote_linter object_usage_linter duplicate_argument_linter + #nolint start: object_usage_linter duplicate_argument_linter issue_location <- paste(names(first_issue), "==", first_issue) cli_abort( c(`!` = "Found incomplete forecasts", diff --git a/R/class-forecast-ordinal.R b/R/class-forecast-ordinal.R index 4a2c86b28..cc07d4bb1 100644 --- a/R/class-forecast-ordinal.R +++ b/R/class-forecast-ordinal.R @@ -106,7 +106,7 @@ assert_forecast.forecast_ordinal <- function( if (!all(complete$correct)) { first_issue <- complete[(correct), ..forecast_unit][1] first_issue <- lapply(first_issue, FUN = as.character) - #nolint start: keyword_quote_linter object_usage_linter duplicate_argument_linter + #nolint start: object_usage_linter duplicate_argument_linter issue_location <- paste(names(first_issue), "==", first_issue) cli_abort( c(`!` = "Found incomplete forecasts", diff --git a/R/class-forecast-point.R b/R/class-forecast-point.R index 8635b25b9..31a1a8167 100644 --- a/R/class-forecast-point.R +++ b/R/class-forecast-point.R @@ -53,12 +53,12 @@ assert_forecast.forecast_point <- function( ) { forecast <- assert_forecast_generic(forecast, verbose) assert_forecast_type(forecast, actual = "point", desired = forecast_type) - #nolint start: keyword_quote_linter object_usage_linter + #nolint start: object_usage_linter input_check <- check_input_point(forecast$observed, forecast$predicted) if (!isTRUE(input_check)) { cli_abort( c( - "!" = "Checking `forecast`: Input looks like a point forecast, but found + `!` = "Checking `forecast`: Input looks like a point forecast, but found the following issue: {input_check}" ) ) diff --git a/R/class-forecast.R b/R/class-forecast.R index 2d9e46d70..c32f8db6c 100644 --- a/R/class-forecast.R +++ b/R/class-forecast.R @@ -79,13 +79,11 @@ assert_forecast.default <- function( forecast, forecast_type = NULL, verbose = TRUE, ... ) { cli_abort( - #nolint start: keyword_quote_linter c( - "!" = "The input needs to be a valid forecast object.", - "i" = "Please convert to `forecast` object first by calling the + `!` = "The input needs to be a valid forecast object.", + `i` = "Please convert to `forecast` object first by calling the appropriate {.fn as_forecast_} function)." ) - #nolint end ) } @@ -117,7 +115,7 @@ assert_forecast_generic <- function(data, verbose = TRUE) { if (problem) { cli_abort( c( - "!" = "Found columns `quantile_level` and `sample_id`. + `!` = "Found columns `quantile_level` and `sample_id`. Only one of these is allowed" ) ) @@ -136,10 +134,9 @@ assert_forecast_generic <- function(data, verbose = TRUE) { # check whether there are any NA values if (anyNA(data)) { if (nrow(na.omit(data)) == 0) { - #nolint start: keyword_quote_linter cli_abort( c( - "!" = "After removing rows with NA values in the data, no forecasts + `!` = "After removing rows with NA values in the data, no forecasts are left." ) ) @@ -147,12 +144,11 @@ assert_forecast_generic <- function(data, verbose = TRUE) { if (verbose) { cli_inform( c( - "i" = "Some rows containing NA values may be removed. + `i` = "Some rows containing NA values may be removed. This is fine if not unexpected." ) ) } - #nolint end } return(data[]) @@ -292,15 +288,13 @@ is_forecast <- function(x) { ) if (inherits(validation, "try-error")) { cli_warn( - #nolint start: keyword_quote_linter c( - "!" = "Error in validating forecast object: {validation}.", - "i" = "Note this error is sometimes related to `data.table`s `print`. + `!` = "Error in validating forecast object: {validation}.", + `i` = "Note this error is sometimes related to `data.table`s `print`. Run {.help [{.fun assert_forecast}](scoringutils::assert_forecast)} to confirm. To get rid of this warning entirely, call `as.data.table()` on the forecast object." ) - #nolint end ) } } @@ -323,7 +317,7 @@ is_forecast <- function(x) { if (inherits(validation, "try-error")) { cli_warn( c( - "!" = "Error in validating forecast object: {validation}" + `!` = "Error in validating forecast object: {validation}" ) ) } @@ -346,7 +340,7 @@ is_forecast <- function(x) { if (inherits(validation, "try-error")) { cli_warn( c( - "!" = "Error in validating forecast object: {validation}" + `!` = "Error in validating forecast object: {validation}" ) ) } @@ -369,7 +363,7 @@ is_forecast <- function(x) { if (inherits(validation, "try-error")) { cli_warn( c( - "!" = "Error in validating forecast object: {validation}" + `!` = "Error in validating forecast object: {validation}" ) ) } @@ -428,7 +422,7 @@ print.forecast <- function(x, ...) { if (inherits(forecast_type, "try-error")) { cli_inform( c( - "!" = "Could not determine forecast type due to error in validation." #nolint + `!` = "Could not determine forecast type due to error in validation." ) ) } else { @@ -443,7 +437,7 @@ print.forecast <- function(x, ...) { if (inherits(forecast_unit, "try-error")) { cli_inform( c( - "!" = "Could not determine forecast unit." #nolint + `!` = "Could not determine forecast unit." ) ) } else { diff --git a/R/class-scores.R b/R/class-scores.R index a00278d6f..8eb718cb1 100644 --- a/R/class-scores.R +++ b/R/class-scores.R @@ -114,25 +114,23 @@ get_metrics.scores <- function(x, error = FALSE, ...) { assert_data_frame(x) metrics <- attr(x, "metrics") if (error && is.null(metrics)) { - #nolint start: keyword_quote_linter cli_abort( c( - "!" = "Input needs an attribute `metrics` with the names of the + `!` = "Input needs an attribute `metrics` with the names of the scoring rules that were used for scoring.", - "i" = "See `?get_metrics` for further information." + `i` = "See `?get_metrics` for further information." ) ) - #nolint end } if (!all(metrics %in% names(x))) { - #nolint start: keyword_quote_linter object_usage_linter + #nolint start: object_usage_linter missing <- setdiff(metrics, names(x)) cli_warn( c( - "!" = "The following scores have been previously computed, but are no + `!` = "The following scores have been previously computed, but are no longer column names of the data: {.val {missing}}", - "i" = "See {.code ?get_metrics} for further information." + `i` = "See {.code ?get_metrics} for further information." ) ) #nolint end diff --git a/R/get-correlations.R b/R/get-correlations.R index 77892bd63..00ad41bf4 100644 --- a/R/get-correlations.R +++ b/R/get-correlations.R @@ -87,14 +87,12 @@ plot_correlations <- function(correlations, digits = NULL) { # check correlations is actually a matrix of correlations col_present <- check_columns_present(correlations, "metric") if (any(lower_triangle > 1, na.rm = TRUE) || !isTRUE(col_present)) { - #nolint start: keyword_quote_linter cli_abort( c( "Found correlations > 1 or missing `metric` column.", - "i" = "Did you forget to call {.fn scoringutils::get_correlations}?" + `i` = "Did you forget to call {.fn scoringutils::get_correlations}?" ) ) - #nolint end } rownames(lower_triangle) <- colnames(lower_triangle) diff --git a/R/get-forecast-type.R b/R/get-forecast-type.R index fde1caa19..5a1310ecf 100644 --- a/R/get-forecast-type.R +++ b/R/get-forecast-type.R @@ -29,12 +29,12 @@ assert_forecast_type <- function(data, desired = NULL) { assert_character(desired, null.ok = TRUE) if (!is.null(desired) && desired != actual) { - #nolint start: object_usage_linter keyword_quote_linter + #nolint start: object_usage_linter cli_abort( c( - "!" = "Forecast type determined by scoringutils based on input: + `!` = "Forecast type determined by scoringutils based on input: {.val {actual}}.", - "i" = "Desired forecast type: {.val {desired}}." + `i` = "Desired forecast type: {.val {desired}}." ) ) #nolint end diff --git a/R/get-pit-histogram.R b/R/get-pit-histogram.R index 1b9167ed8..5ae10435d 100644 --- a/R/get-pit-histogram.R +++ b/R/get-pit-histogram.R @@ -58,6 +58,6 @@ get_pit_histogram <- function(forecast, num_bins, breaks, by, #' @export get_pit_histogram.default <- function(forecast, num_bins, breaks, by, ...) { cli_abort(c( - "!" = "The input needs to be a valid forecast object represented as quantiles or samples." # nolint + `!` = "The input needs to be a valid forecast object represented as quantiles or samples." )) } diff --git a/R/helper-quantile-interval-range.R b/R/helper-quantile-interval-range.R index cba7b5ff7..da1423c2a 100644 --- a/R/helper-quantile-interval-range.R +++ b/R/helper-quantile-interval-range.R @@ -82,12 +82,10 @@ quantile_to_interval_dataframe <- function(forecast, if (length(unique(forecast$boundary)) < 2) { cli_abort( c( - #nolint start: keyword_quote_linter `!` = "No valid forecast intervals found.", `i` = "A forecast interval comprises two quantiles with quantile levels symmetric around the median (e.g. 0.25 and 0.75)" - #nolint end ) ) } diff --git a/R/metrics-interval-range.R b/R/metrics-interval-range.R index 947e05c30..0a994432e 100644 --- a/R/metrics-interval-range.R +++ b/R/metrics-interval-range.R @@ -32,25 +32,23 @@ assert_input_interval <- function(observed, lower, upper, interval_range) { if (any(diff < 0)) { cli_abort( c( - "!" = "All values in `upper` need to be greater than or equal to + `!` = "All values in `upper` need to be greater than or equal to the corresponding values in `lower`" ) ) } if (any(interval_range > 0 & interval_range < 1, na.rm = TRUE)) { - #nolint start: keyword_quote_linter cli_warn( c( - "!" = "Found interval ranges between 0 and 1. Are you sure that's + `!` = "Found interval ranges between 0 and 1. Are you sure that's right? An interval range of 0.5 e.g. implies a (49.75%, 50.25%) prediction interval.", - "i" = "If you want to score a (25%, 75%) prediction interval, set + `i` = "If you want to score a (25%, 75%) prediction interval, set `interval_range = 50`." ), .frequency = "once", .frequency_id = "small_interval_range" ) - #nolint end } return(invisible(NULL)) } diff --git a/R/metrics-nominal.R b/R/metrics-nominal.R index 4a8b0a89f..5a550191b 100644 --- a/R/metrics-nominal.R +++ b/R/metrics-nominal.R @@ -75,7 +75,7 @@ assert_input_categorical <- function( # Allow for numeric errors invalid_rows <- abs(summed_predictions - 1) > 1e-4 if (any(invalid_rows)) { - #nolint start: keyword_quote_linter object_usage_linter + #nolint start: object_usage_linter row_indices <- which(invalid_rows) cli_abort( c( diff --git a/R/metrics-point.R b/R/metrics-point.R index f31dc9030..dab5fca0f 100644 --- a/R/metrics-point.R +++ b/R/metrics-point.R @@ -49,12 +49,12 @@ assert_dims_ok_point <- function(observed, predicted) { n_pred <- length(as.vector(predicted)) # check that both are either of length 1 or of equal length if ((n_obs != 1) && (n_pred != 1) && (n_obs != n_pred)) { - #nolint start: keyword_quote_linter object_usage_linter + #nolint start: object_usage_linter cli_abort( c( - "!" = "`observed` and `predicted` must either be of length 1 or + `!` = "`observed` and `predicted` must either be of length 1 or of equal length.", - "i" = "Found {n_obs} and {n_pred}." + `i` = "Found {n_obs} and {n_pred}." ) ) #nolint end diff --git a/R/metrics-quantile.R b/R/metrics-quantile.R index fd5775076..b72225f8f 100644 --- a/R/metrics-quantile.R +++ b/R/metrics-quantile.R @@ -191,11 +191,11 @@ wis <- function(observed, complete_intervals <- duplicated(interval_ranges) | duplicated(interval_ranges, fromLast = TRUE) if (!all(complete_intervals) && !isTRUE(na.rm)) { - #nolint start: keyword_quote_linter object_usage_linter + #nolint start: object_usage_linter incomplete <- quantile_level[quantile_level != 0.5][!complete_intervals] cli_abort( c( - "!" = "Not all quantile levels specified form symmetric prediction + `!` = "Not all quantile levels specified form symmetric prediction intervals. The following quantile levels miss a corresponding lower/upper bound: {.val {incomplete}}. @@ -347,10 +347,10 @@ interval_coverage <- function(observed, predicted, 100 - (100 - interval_range) / 2 ) / 100 if (!all(necessary_quantiles %in% quantile_level)) { - #nolint start: keyword_quote_linter object_usage_linter + #nolint start: object_usage_linter cli_abort( c( - "!" = "To compute the interval coverage for an interval range of + `!` = "To compute the interval coverage for an interval range of {.val {interval_range}%}, the {.val {necessary_quantiles}} quantiles are required" ) @@ -442,14 +442,12 @@ bias_quantile <- function(observed, predicted, quantile_level, na.rm = TRUE) { dim(predicted) <- c(n, N) } if (!(0.5 %in% quantile_level)) { - #nolint start: keyword_quote_linter cli_inform( c( - "i" = "Median not available, interpolating median from the two + `i` = "Median not available, interpolating median from the two innermost quantiles in order to compute bias." ) ) - #nolint end } bias <- sapply(1:n, function(i) { bias_quantile_single_vector( @@ -493,14 +491,12 @@ bias_quantile_single_vector <- function(observed, predicted, order <- order(quantile_level) predicted <- predicted[order] if (!all(diff(predicted) >= 0)) { - #nolint start: keyword_quote_linter cli_abort( c( - "!" = "Predictions must not be decreasing with increasing + `!` = "Predictions must not be decreasing with increasing quantile level." ) ) - #nolint end } median_prediction <- interpolate_median(predicted, quantile_level) @@ -581,14 +577,12 @@ interpolate_median <- function(predicted, quantile_level) { ae_median_quantile <- function(observed, predicted, quantile_level) { assert_input_quantile(observed, predicted, quantile_level) if (!any(quantile_level == 0.5)) { - #nolint start: keyword_quote_linter cli_abort( c( - "!" = "In order to compute the absolute error of the median, + `!` = "In order to compute the absolute error of the median, {.val 0.5} must be among the quantiles given" ) ) - #nolint end } if (is.null(dim(predicted))) { predicted <- matrix(predicted, nrow = 1) diff --git a/R/pairwise-comparisons.R b/R/pairwise-comparisons.R index 2e8b00b10..70fe6fef9 100644 --- a/R/pairwise-comparisons.R +++ b/R/pairwise-comparisons.R @@ -137,13 +137,13 @@ get_pairwise_comparisons <- function( assert_character(metric, len = 1) # check that columns in 'by' are present - #nolint start: keyword_quote_linter object_usage_linter + #nolint start: object_usage_linter if (length(by) > 0) { by_cols <- check_columns_present(scores, by) if (!isTRUE(by_cols)) { cli_abort( c( - "!" = "Not all columns specified in `by` are present: {.var {by_cols}}" + `!` = "Not all columns specified in `by` are present: {.var {by_cols}}" ) ) #nolint end @@ -159,33 +159,31 @@ get_pairwise_comparisons <- function( # check there are enough comparators if (length(setdiff(comparators, baseline)) < 2) { - #nolint start: keyword_quote_linter cli_abort( c( - "!" = "More than one non-baseline model is needed to compute + `!` = "More than one non-baseline model is needed to compute pairwise compairisons." ) ) - #nolint end } # check that values of the chosen metric are not NA if (anyNA(scores[[metric]])) { scores <- scores[!is.na(scores[[metric]])] if (nrow(scores) == 0) { - #nolint start: keyword_quote_linter object_usage_linter + #nolint start: object_usage_linter cli_abort( c( - "!" = "After removing {.val NA} values for {.var {metric}}, + `!` = "After removing {.val NA} values for {.var {metric}}, no values were left." ) ) } cli_warn( c( - "!" = "Some values for the metric {.var {metric}} + `!` = "Some values for the metric {.var {metric}} are NA. These have been removed.", - "i" = "Maybe choose a different metric?" + `i` = "Maybe choose a different metric?" ) ) #nolint end @@ -193,10 +191,10 @@ get_pairwise_comparisons <- function( # check that all values of the chosen metric are positive if (any(sign(scores[[metric]]) < 0) && any(sign(scores[[metric]]) > 0)) { - #nolint start: keyword_quote_linter object_usage_linter + #nolint start: object_usage_linter cli_abort( c( - "!" = "To compute pairwise comparisons, all values of {.var {metric}} + `!` = "To compute pairwise comparisons, all values of {.var {metric}} must have the same sign." ) ) @@ -210,25 +208,21 @@ get_pairwise_comparisons <- function( # sense # if compare == forecast_unit then all relative skill scores will simply be 1. if (setequal(compare, forecast_unit)) { - #nolint start: keyword_quote_linter cli_warn( c( - "!" = "`compare` is set to the unit of a single forecast. This doesn't + `!` = "`compare` is set to the unit of a single forecast. This doesn't look right.", - "i" = "All relative skill scores will be equal to 1." + `i` = "All relative skill scores will be equal to 1." ) ) - #nolint end } else if (setequal(c(compare, by), forecast_unit)) { - #nolint start: keyword_quote_linter cli_inform( c( - "!" = "relative skill can only be computed if the combination of + `!` = "relative skill can only be computed if the combination of `compare` and `by` is different from the unit of a single forecast.", - "i" = "`by` was set to an empty character vector" + `i` = "`by` was set to an empty character vector" ) ) - #nolint end by <- character(0) } @@ -288,7 +282,7 @@ pairwise_comparison_one_group <- function(scores, # if there aren't enough models to do any comparison, abort if (length(comparators) < 2) { cli_abort( - c("!" = "There are not enough comparators to do any comparison") + c(`!` = "There are not enough comparators to do any comparison") ) } diff --git a/R/score.R b/R/score.R index 7f4eb8f7a..66b2a7b11 100644 --- a/R/score.R +++ b/R/score.R @@ -123,13 +123,11 @@ score <- function(forecast, metrics, ...) { #' @export score.default <- function(forecast, metrics, ...) { cli_abort( - #nolint start: keyword_quote_linter c( - "!" = "The input needs to be a valid forecast object.", - "i" = "Please convert to a `forecast` object first by calling the + `!` = "The input needs to be a valid forecast object.", + `i` = "Please convert to a `forecast` object first by calling the appropriate {.fn as_forecast_} function)." ) - #nolint end ) } @@ -213,7 +211,7 @@ run_safely <- function(..., fun, metric_name) { msg <- conditionMessage(attr(result, "condition")) cli_warn( c( - "!" = "Computation for {.var {metric_name}} failed. + `!` = "Computation for {.var {metric_name}} failed. Error: {msg}." ) ) @@ -249,13 +247,11 @@ validate_metrics <- function(metrics) { for (i in seq_along(metrics)) { check_fun <- check_function(metrics[[i]]) if (!isTRUE(check_fun)) { - #nolint start: keyword_quote_linter cli_warn( c( - "!" = "`Metrics` element number {i} is not a valid function." + `!` = "`Metrics` element number {i} is not a valid function." ) ) - #nolint end names(metrics)[i] <- "scoringutils_delete" } } diff --git a/R/transform-forecasts.R b/R/transform-forecasts.R index 7e9c2b071..7180aafe8 100644 --- a/R/transform-forecasts.R +++ b/R/transform-forecasts.R @@ -124,7 +124,6 @@ transform_forecasts <- function(forecast, # Error handling if (scale_col_present) { if (!("natural" %in% original_forecast$scale)) { - #nolint start: keyword_quote_linter cli_abort( c( `!` = "If a column 'scale' is present, entries with scale =='natural' @@ -135,11 +134,10 @@ transform_forecasts <- function(forecast, if (append && (label %in% original_forecast$scale)) { cli_warn( c( - "i" = "Appending new transformations with label '{label}' + `i` = "Appending new transformations with label '{label}' even though that entry is already present in column 'scale'." ) ) - #nolint end } } @@ -217,21 +215,19 @@ log_shift <- function(x, offset = 0, base = exp(1)) { assert_number(base, lower = 0) if (any(x < 0, na.rm = TRUE)) { - #nolint start: keyword_quote_linter cli_abort( c( - "!" = "Detected input values < 0." + `!` = "Detected input values < 0." ) ) } if (any(x == 0, na.rm = TRUE) && offset == 0) { cli_warn( c( - "!" = "Detected zeros in input values.", - "i" = "Try specifying offset = 1 (or any other offset)." + `!` = "Detected zeros in input values.", + `i` = "Try specifying offset = 1 (or any other offset)." ) ) - #nolint end } log(x + offset, base = base) } diff --git a/tests/testthat/test-cli-linting.R b/tests/testthat/test-cli-linting.R new file mode 100644 index 000000000..acf3f1a31 --- /dev/null +++ b/tests/testthat/test-cli-linting.R @@ -0,0 +1,84 @@ +# Helper to find the R/ source directory; works both during devtools::test() +# and R CMD check (where the working directory differs). +find_r_source_dir <- function() { + candidates <- c( + file.path(testthat::test_path(), "..", "..", "R"), + "../../R" + ) + for (d in candidates) { + if (dir.exists(d)) return(d) + } + NULL +} + +test_that("no R source files use double-quoted cli condition names", { + r_dir <- find_r_source_dir() + skip_if(is.null(r_dir), "R/ source directory not found") + r_files <- list.files(r_dir, pattern = "\\.R$", full.names = TRUE) + matches <- character(0) + for (f in r_files) { + lines <- readLines(f, warn = FALSE) + idx <- grep('"[!ixv]"\\s*=', lines) + if (length(idx) > 0) { + matches <- c(matches, paste0(basename(f), ":", idx, ": ", trimws(lines[idx]))) + } + } + expect_length(matches, 0) +}) + +test_that("no keyword_quote_linter nolint annotations remain in R source", { + r_dir <- find_r_source_dir() + skip_if(is.null(r_dir), "R/ source directory not found") + r_files <- list.files(r_dir, pattern = "\\.R$", full.names = TRUE) + matches <- character(0) + for (f in r_files) { + lines <- readLines(f, warn = FALSE) + idx <- grep("keyword_quote_linter", lines) + if (length(idx) > 0) { + matches <- c(matches, paste0(basename(f), ":", idx, ": ", trimws(lines[idx]))) + } + } + expect_length(matches, 0) +}) + +test_that("keyword_quote_linter is not disabled in .lintr config", { + lintr_path <- file.path(testthat::test_path(), "..", "..", ".lintr") + skip_if(!file.exists(lintr_path), ".lintr not found (likely R CMD check)") + lintr_content <- readLines(lintr_path, warn = FALSE) + expect_false(any(grepl("keyword_quote_linter\\s*=\\s*NULL", lintr_content))) +}) + +test_that("cli error and warning messages still render correctly after quoting change", { + expect_error( + assert_forecast(data.frame()), + "valid forecast object" + ) +}) + +test_that("nolint blocks with only keyword_quote_linter are fully removed", { + r_dir <- find_r_source_dir() + skip_if(is.null(r_dir), "R/ source directory not found") + r_files <- list.files(r_dir, pattern = "\\.R$", full.names = TRUE) + keyword_only_blocks <- character(0) + for (f in r_files) { + lines <- readLines(f, warn = FALSE) + start_idx <- grep("#\\s*nolint start", lines) + for (i in start_idx) { + line <- lines[i] + # Check if this nolint start mentions keyword_quote_linter + if (grepl("keyword_quote_linter", line)) { + # Check if it ONLY mentions keyword_quote_linter (no other linters) + cleaned <- sub("#\\s*nolint start:\\s*", "", line) + cleaned <- trimws(cleaned) + linters <- trimws(strsplit(cleaned, "\\s+")[[1]]) + if (length(linters) == 1 && linters[1] == "keyword_quote_linter") { + keyword_only_blocks <- c( + keyword_only_blocks, + paste0(basename(f), ":", i, ": ", trimws(line)) + ) + } + } + } + } + expect_length(keyword_only_blocks, 0) +})