From 0ba3deb7d893b577e756f54ed7e24f9183d782d3 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 17 Feb 2026 09:02:52 +0100 Subject: [PATCH 01/22] feat: first iteration of robustness lints metric --- DESCRIPTION | 2 + R/data_robustness_lints.R | 147 ++++++++++++++++++ .../badge-dep-lintr-x-flat-square-green.svg | 1 + .../badge-dep-lintr-x-flat-square-red.svg | 1 + man/metrics.Rd | 7 + 5 files changed, 158 insertions(+) create mode 100644 R/data_robustness_lints.R create mode 100644 man/figures/badge-dep-lintr-x-flat-square-green.svg create mode 100644 man/figures/badge-dep-lintr-x-flat-square-red.svg diff --git a/DESCRIPTION b/DESCRIPTION index 8a343ba..9d126be 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -50,6 +50,7 @@ Suggests: rcmdcheck, igraph, knitr, + lintr, rmarkdown, rosv, testthat (>= 3.0.0), @@ -94,6 +95,7 @@ Collate: 'data_r_cmd_check.R' 'data_recognized_source.R' 'data_reverse_dependencies.R' + 'data_robustness_lints.R' 'data_vignettes.R' 'data_web_html.R' 'generic_metric_coerce.R' diff --git a/R/data_robustness_lints.R b/R/data_robustness_lints.R new file mode 100644 index 0000000..3d851de --- /dev/null +++ b/R/data_robustness_lints.R @@ -0,0 +1,147 @@ +#' @include impl_data.R + +# Robustness lints metric implementation + +#' Get list of robustness linters to apply +#' +#' Returns a list of lintr robustness linters that identify potentially +#' error-prone code patterns based on relatively unopinionated good programming +#' practices. +#' +#' @return List of linter functions +#' @noRd +get_robustness_linters <- function() { + list( + # Logic errors + lintr::class_equals_linter(), + lintr::equals_na_linter(), + lintr::redundant_equals_linter(), + lintr::vector_logic_linter(), + lintr::outer_negation_linter(), + + # Iteration/sequence safety + lintr::implicit_integer_linter(), + lintr::seq_linter(), + lintr::length_test_linter(), + + # String operations + lintr::nzchar_linter(), + lintr::paste_linter(), + lintr::fixed_regex_linter(), + + # Conditionals + lintr::redundant_ifelse_linter(), + lintr::ifelse_censor_linter(), + lintr::conjunct_test_linter(), + + # Functions/arguments + lintr::duplicate_argument_linter(), + lintr::missing_argument_linter(), + lintr::condition_message_linter(), + + # Code organization + lintr::unreachable_code_linter(), + lintr::library_call_linter(), + lintr::namespace_linter(), + + # Package development + lintr::package_hooks_linter(), + lintr::routine_registration_linter(), + lintr::system_file_linter(), + + # Efficiency + lintr::literal_coercion_linter(), + lintr::scalar_in_linter(), + + # Best practices + lintr::T_and_F_symbol_linter(), + lintr::stopifnot_all_linter() + ) +} + +# Auxiliary data: full lint results +impl_data( + "robustness_lints", + class = class_list, + metric = FALSE, + tags = character(0) +) + +impl_data( + "robustness_lints", + for_resource = source_code_resource, + function(pkg, resource, field, ...) { + if (!requireNamespace("lintr", quietly = TRUE)) { + return(list()) + } + + linters <- get_robustness_linters() + + # Lint the package source + lints <- lintr::lint_package( + path = resource@path, + linters = linters, + parse_settings = FALSE # Don't read .lintr config for consistency + ) + + # Convert to list (lintr returns special S3 class) + as.list(lints) + } +) + +impl_data( + "robustness_lints", + for_resource = mock_resource, + function(pkg, resource, field, ...) { + # Generate 0-10 mock lints (Poisson distribution, lambda = 3) + n_lints <- rpois(1L, lambda = 3) + + if (n_lints == 0L) { + return(list()) + } + + # Mock lint object structure + replicate(n_lints, list( + filename = "R/mock.R", + line_number = sample(1L:100L, 1L), + column_number = 1L, + type = "warning", + message = "Mock robustness lint", + line = "# mock code", + linter = "mock_linter" + ), simplify = FALSE) + } +) + +# Primary metric: count of robustness lints +impl_data( + "robustness_lint_count", + metric = TRUE, + class = class_integer, + tags = tags("best practice"), + suggests = suggests("lintr"), + title = "Robustness Lint Count", + description = paste( + "Count of robustness lints identified by \\pkg{lintr}.", + "Robustness lints identify potentially error-prone code patterns", + "based on relatively unopinionated good programming practices.", + "Lower counts indicate more robust code.", + "See \\url{https://lintr.r-lib.org/reference/robustness_linters.html}" + ) +) + +impl_data( + "robustness_lint_count", + for_resource = source_code_resource, + function(pkg, resource, field, ...) { + length(pkg$robustness_lints) + } +) + +impl_data( + "robustness_lint_count", + for_resource = mock_resource, + function(pkg, resource, field, ...) { + length(pkg$robustness_lints) + } +) diff --git a/man/figures/badge-dep-lintr-x-flat-square-green.svg b/man/figures/badge-dep-lintr-x-flat-square-green.svg new file mode 100644 index 0000000..fcb5e18 --- /dev/null +++ b/man/figures/badge-dep-lintr-x-flat-square-green.svg @@ -0,0 +1 @@ +dep: lintrdeplintr \ No newline at end of file diff --git a/man/figures/badge-dep-lintr-x-flat-square-red.svg b/man/figures/badge-dep-lintr-x-flat-square-red.svg new file mode 100644 index 0000000..652fc9c --- /dev/null +++ b/man/figures/badge-dep-lintr-x-flat-square-red.svg @@ -0,0 +1 @@ +dep: lintrdeplintr \ No newline at end of file diff --git a/man/metrics.Rd b/man/metrics.Rd index 3769bb6..719958f 100644 --- a/man/metrics.Rd +++ b/man/metrics.Rd @@ -27,6 +27,13 @@ For access to \emph{all} the internally calculated data, pass \code{all = TRUE}. } \keyword{workflow} \section{Metrics}{The following metrics are provided by \code{\link{val.meter}}. + \subsection{Robustness Lint Count}{ +\code{} Count of robustness lints identified by \pkg{lintr}. Robustness lints identify potentially error-prone code patterns based on relatively unopinionated good programming practices. Lower counts indicate more robust code. See \url{https://lintr.r-lib.org/reference/robustness_linters.html} + + \Sexpr[stage=render,results=rd]{if (length(find.package("lintr", quiet = TRUE)) > 0) "\\\\ifelse{html}{\\\\figure{badge-dep-lintr-x-flat-square-green.svg}{options: alt = \\"[lintr]\\"}}{\\\\strong{[dep::lintr]}}" else "\\\\ifelse{html}{\\\\figure{badge-dep-lintr-x-flat-square-red.svg}{options: alt = \\"[lintr]\\"}}{\\\\strong{[dep::lintr]}}"} + +\Sexpr[stage=install,results=rd]{if (numeric_version(paste0(R.version$major, ".", R.version$minor)) < "4.5.0") { "\\\\ifelse{html}{\\\\figure{badge-best_practice-x-flat-square-blue.svg}{options: alt = \\"[best practice]\\"}}{\\\\strong{[best practice]}}" } else { "\\\\link[val.meter:tags]{\\\\ifelse{html}{\\\\figure{badge-best_practice-x-flat-square-blue.svg}{options: alt = \\"[best practice]\\"}}{\\\\strong{[best practice]}}}" }} +} \subsection{Up to date NEWS}{ \code{} a NEWS file exists and is synced with the current package version From 7454e04c208aeeff8d6e5efcb93986fb2ecfc768 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Fri, 20 Feb 2026 13:29:47 +0100 Subject: [PATCH 02/22] refactor: simplify selection of metrics and wrap result --- R/data_robustness_lints.R | 120 ++++++-------------------------------- 1 file changed, 19 insertions(+), 101 deletions(-) diff --git a/R/data_robustness_lints.R b/R/data_robustness_lints.R index 3d851de..84ad452 100644 --- a/R/data_robustness_lints.R +++ b/R/data_robustness_lints.R @@ -2,114 +2,35 @@ # Robustness lints metric implementation -#' Get list of robustness linters to apply -#' -#' Returns a list of lintr robustness linters that identify potentially -#' error-prone code patterns based on relatively unopinionated good programming -#' practices. -#' -#' @return List of linter functions -#' @noRd -get_robustness_linters <- function() { - list( - # Logic errors - lintr::class_equals_linter(), - lintr::equals_na_linter(), - lintr::redundant_equals_linter(), - lintr::vector_logic_linter(), - lintr::outer_negation_linter(), - - # Iteration/sequence safety - lintr::implicit_integer_linter(), - lintr::seq_linter(), - lintr::length_test_linter(), - - # String operations - lintr::nzchar_linter(), - lintr::paste_linter(), - lintr::fixed_regex_linter(), - - # Conditionals - lintr::redundant_ifelse_linter(), - lintr::ifelse_censor_linter(), - lintr::conjunct_test_linter(), - - # Functions/arguments - lintr::duplicate_argument_linter(), - lintr::missing_argument_linter(), - lintr::condition_message_linter(), - - # Code organization - lintr::unreachable_code_linter(), - lintr::library_call_linter(), - lintr::namespace_linter(), - - # Package development - lintr::package_hooks_linter(), - lintr::routine_registration_linter(), - lintr::system_file_linter(), - - # Efficiency - lintr::literal_coercion_linter(), - lintr::scalar_in_linter(), - - # Best practices - lintr::T_and_F_symbol_linter(), - lintr::stopifnot_all_linter() - ) -} - # Auxiliary data: full lint results impl_data( "robustness_lints", - class = class_list, - metric = FALSE, - tags = character(0) + class = S7::new_S3_class("lints"), + tags = c("execution"), + suggests = "lintr", + permissions = "execution", ) impl_data( "robustness_lints", for_resource = source_code_resource, - function(pkg, resource, field, ...) { - if (!requireNamespace("lintr", quietly = TRUE)) { - return(list()) + function(pkg, resource, field, ..., quiet = opt("quiet")) { + wrapper <- if (quiet) { + function(...) capture.output(..., type = "message") + } else { + identity } - linters <- get_robustness_linters() - - # Lint the package source - lints <- lintr::lint_package( - path = resource@path, - linters = linters, - parse_settings = FALSE # Don't read .lintr config for consistency - ) - - # Convert to list (lintr returns special S3 class) - as.list(lints) - } -) + wrapper({ + # Suppress lintr messages to avoid stdout output from subprocess + result <- lintr::lint_package( + path = resource@path, + linters = lintr::linters_with_tags("robustness"), + parse_settings = FALSE # Don't read .lintr config for consistency + ) + }) -impl_data( - "robustness_lints", - for_resource = mock_resource, - function(pkg, resource, field, ...) { - # Generate 0-10 mock lints (Poisson distribution, lambda = 3) - n_lints <- rpois(1L, lambda = 3) - - if (n_lints == 0L) { - return(list()) - } - - # Mock lint object structure - replicate(n_lints, list( - filename = "R/mock.R", - line_number = sample(1L:100L, 1L), - column_number = 1L, - type = "warning", - message = "Mock robustness lint", - line = "# mock code", - linter = "mock_linter" - ), simplify = FALSE) + result } ) @@ -119,7 +40,6 @@ impl_data( metric = TRUE, class = class_integer, tags = tags("best practice"), - suggests = suggests("lintr"), title = "Robustness Lint Count", description = paste( "Count of robustness lints identified by \\pkg{lintr}.", @@ -141,7 +61,5 @@ impl_data( impl_data( "robustness_lint_count", for_resource = mock_resource, - function(pkg, resource, field, ...) { - length(pkg$robustness_lints) - } + function(pkg, resource, field, ...) rpois(1, 10) ) From 923c2b909139d7a78bb6b26083704c0ea66bb7fc Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 24 Feb 2026 15:27:49 +0100 Subject: [PATCH 03/22] refactor: adjust metrics for consistency --- R/data_robustness_lints.R | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/R/data_robustness_lints.R b/R/data_robustness_lints.R index 84ad452..213c1ec 100644 --- a/R/data_robustness_lints.R +++ b/R/data_robustness_lints.R @@ -6,9 +6,17 @@ impl_data( "robustness_lints", class = S7::new_S3_class("lints"), + metric = FALSE, # Explicitly mark as auxiliary data, not a metric tags = c("execution"), suggests = "lintr", permissions = "execution", + title = "Robustness Lint Results", + description = paste( + "Full \\pkg{lintr} robustness lint results for the package.", + "Contains detailed information about each lint including line numbers,", + "column positions, and lint messages. This is auxiliary data used to", + "derive the \\code{robustness_lint_count} metric." + ) ) impl_data( @@ -20,13 +28,13 @@ impl_data( } else { identity } - + wrapper({ # Suppress lintr messages to avoid stdout output from subprocess result <- lintr::lint_package( path = resource@path, linters = lintr::linters_with_tags("robustness"), - parse_settings = FALSE # Don't read .lintr config for consistency + parse_settings = FALSE # Don't read .lintr config for consistency ) }) @@ -39,7 +47,7 @@ impl_data( "robustness_lint_count", metric = TRUE, class = class_integer, - tags = tags("best practice"), + tags = c("best practice"), # Use c() for consistency title = "Robustness Lint Count", description = paste( "Count of robustness lints identified by \\pkg{lintr}.", From 38b1f35e49d476f9ec13af20e6a1afb2b3fc5962 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 24 Feb 2026 15:39:34 +0100 Subject: [PATCH 04/22] test: add basic tests for lint metric --- tests/testthat/test-data_robustness_lints.R | 135 ++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 tests/testthat/test-data_robustness_lints.R diff --git a/tests/testthat/test-data_robustness_lints.R b/tests/testthat/test-data_robustness_lints.R new file mode 100644 index 0000000..108fddc --- /dev/null +++ b/tests/testthat/test-data_robustness_lints.R @@ -0,0 +1,135 @@ +# Helper: create a fake lintr `lints` object with n entries +create_mock_lints <- function(n = 3) { + lint_list <- replicate(n, structure( + list( + filename = "R/test.R", + line_number = 1L, + column_number = 1L, + type = "warning", + message = "mock robustness lint", + line = "x = 1", + ranges = NULL + ), + class = "lint" + ), simplify = FALSE) + + structure(lint_list, class = c("lints", "list")) +} + +# Helper: create a minimal package source directory for source_code_resource +create_mock_pkg_dir <- function(dir) { + dir.create(file.path(dir, "R"), recursive = TRUE) + writeLines( + c( + "Package: mockpkg", + "Version: 1.0.0", + "Title: Mock Package", + "Description: A mock package for testing.", + "Authors@R: person('A', 'B', role = 'aut')", + "License: MIT + file LICENSE" + ), + file.path(dir, "DESCRIPTION") + ) + writeLines("x <- 1", file.path(dir, "R", "mock.R")) + invisible(dir) +} + +describe("robustness_lint_count derivation from mocked lints", { + it("returns 0 when there are no lints", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(0), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + result <- p$robustness_lint_count + expect_equal(result, 0L) + } + ) + }) + + it("returns the correct count matching number of lints", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(7), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + result <- p$robustness_lint_count + expect_equal(result, 7L) + } + ) + }) + + it("returns a non-negative integer", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(4), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + result <- p$robustness_lint_count + expect_true(is.numeric(result)) + expect_true(result >= 0) + } + ) + }) + + it("stores the full lints object in robustness_lints", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(3), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + lints <- p$robustness_lints + expect_true(inherits(lints, "lints")) + expect_length(lints, 3) + } + ) + }) +}) + +describe("robustness_lint_count mock implementation", { + it("returns non-negative values", { + results <- replicate(50, { + p <- random_pkg() + p$robustness_lint_count + }) + + expect_true(all(results >= 0)) + }) + + it("returns numeric values consistently", { + results <- replicate(20, { + p <- random_pkg() + p$robustness_lint_count + }) + + expect_true(all(vlapply(as.list(results), is.numeric))) + }) +}) From 5d3f14c3d457bed79d4dd4da26245fb275410bff Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 25 Feb 2026 11:26:58 +0100 Subject: [PATCH 05/22] feat: expand scope of robustness lints to correctness --- DESCRIPTION | 1 - ...tness_lints.R => data_correctness_lints.R} | 42 ++++++++++--------- ..._lints.R => test-data_correctness_lints.R} | 20 ++++----- 3 files changed, 32 insertions(+), 31 deletions(-) rename R/{data_robustness_lints.R => data_correctness_lints.R} (53%) rename tests/testthat/{test-data_robustness_lints.R => test-data_correctness_lints.R} (87%) diff --git a/DESCRIPTION b/DESCRIPTION index 9d126be..ea63612 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -95,7 +95,6 @@ Collate: 'data_r_cmd_check.R' 'data_recognized_source.R' 'data_reverse_dependencies.R' - 'data_robustness_lints.R' 'data_vignettes.R' 'data_web_html.R' 'generic_metric_coerce.R' diff --git a/R/data_robustness_lints.R b/R/data_correctness_lints.R similarity index 53% rename from R/data_robustness_lints.R rename to R/data_correctness_lints.R index 213c1ec..db80c71 100644 --- a/R/data_robustness_lints.R +++ b/R/data_correctness_lints.R @@ -1,26 +1,26 @@ #' @include impl_data.R -# Robustness lints metric implementation +# Correctness lints metric implementation # Auxiliary data: full lint results impl_data( - "robustness_lints", + "correctness_lints", class = S7::new_S3_class("lints"), metric = FALSE, # Explicitly mark as auxiliary data, not a metric tags = c("execution"), suggests = "lintr", permissions = "execution", - title = "Robustness Lint Results", + title = "Correctness Lint Results", description = paste( - "Full \\pkg{lintr} robustness lint results for the package.", + "Full \\pkg{lintr} correctness lint results for the package.", "Contains detailed information about each lint including line numbers,", "column positions, and lint messages. This is auxiliary data used to", - "derive the \\code{robustness_lint_count} metric." + "derive the \\code{correctness_lint_count} metric." ) ) impl_data( - "robustness_lints", + "correctness_lints", for_resource = source_code_resource, function(pkg, resource, field, ..., quiet = opt("quiet")) { wrapper <- if (quiet) { @@ -30,10 +30,11 @@ impl_data( } wrapper({ - # Suppress lintr messages to avoid stdout output from subprocess result <- lintr::lint_package( path = resource@path, - linters = lintr::linters_with_tags("robustness"), + linters = lintr::linters_with_tags( + tags = c("correctness", "common_mistakes", "robustness") + ), parse_settings = FALSE # Don't read .lintr config for consistency ) }) @@ -42,32 +43,33 @@ impl_data( } ) -# Primary metric: count of robustness lints +# Primary metric: count of correctness lints impl_data( - "robustness_lint_count", + "correctness_lint_count", metric = TRUE, class = class_integer, - tags = c("best practice"), # Use c() for consistency - title = "Robustness Lint Count", + tags = c("best practice"), + title = "Correctness Lint Count", description = paste( - "Count of robustness lints identified by \\pkg{lintr}.", - "Robustness lints identify potentially error-prone code patterns", - "based on relatively unopinionated good programming practices.", - "Lower counts indicate more robust code.", - "See \\url{https://lintr.r-lib.org/reference/robustness_linters.html}" + "Count of correctness lints identified by \\pkg{lintr}.", + "Combines linters from the \\emph{correctness}, \\emph{common_mistakes},", + "and \\emph{robustness} tags to identify potentially error-prone code", + "patterns and common programming mistakes.", + "Lower counts indicate higher code quality.", + "See \\url{https://lintr.r-lib.org/reference/correctness_linters.html}" ) ) impl_data( - "robustness_lint_count", + "correctness_lint_count", for_resource = source_code_resource, function(pkg, resource, field, ...) { - length(pkg$robustness_lints) + length(pkg$correctness_lints) } ) impl_data( - "robustness_lint_count", + "correctness_lint_count", for_resource = mock_resource, function(pkg, resource, field, ...) rpois(1, 10) ) diff --git a/tests/testthat/test-data_robustness_lints.R b/tests/testthat/test-data_correctness_lints.R similarity index 87% rename from tests/testthat/test-data_robustness_lints.R rename to tests/testthat/test-data_correctness_lints.R index 108fddc..bf19398 100644 --- a/tests/testthat/test-data_robustness_lints.R +++ b/tests/testthat/test-data_correctness_lints.R @@ -6,7 +6,7 @@ create_mock_lints <- function(n = 3) { line_number = 1L, column_number = 1L, type = "warning", - message = "mock robustness lint", + message = "mock correctness lint", line = "x = 1", ranges = NULL ), @@ -34,7 +34,7 @@ create_mock_pkg_dir <- function(dir) { invisible(dir) } -describe("robustness_lint_count derivation from mocked lints", { +describe("correctness_lint_count derivation from mocked lints", { it("returns 0 when there are no lints", { skip_if_not_installed("lintr") tmp_dir <- withr::local_tempdir() @@ -48,7 +48,7 @@ describe("robustness_lint_count derivation from mocked lints", { source_code_resource(path = tmp_dir, package = "mockpkg"), permissions = permissions("execution") ) - result <- p$robustness_lint_count + result <- p$correctness_lint_count expect_equal(result, 0L) } ) @@ -67,7 +67,7 @@ describe("robustness_lint_count derivation from mocked lints", { source_code_resource(path = tmp_dir, package = "mockpkg"), permissions = permissions("execution") ) - result <- p$robustness_lint_count + result <- p$correctness_lint_count expect_equal(result, 7L) } ) @@ -86,14 +86,14 @@ describe("robustness_lint_count derivation from mocked lints", { source_code_resource(path = tmp_dir, package = "mockpkg"), permissions = permissions("execution") ) - result <- p$robustness_lint_count + result <- p$correctness_lint_count expect_true(is.numeric(result)) expect_true(result >= 0) } ) }) - it("stores the full lints object in robustness_lints", { + it("stores the full lints object in correctness_lints", { skip_if_not_installed("lintr") tmp_dir <- withr::local_tempdir() create_mock_pkg_dir(tmp_dir) @@ -106,7 +106,7 @@ describe("robustness_lint_count derivation from mocked lints", { source_code_resource(path = tmp_dir, package = "mockpkg"), permissions = permissions("execution") ) - lints <- p$robustness_lints + lints <- p$correctness_lints expect_true(inherits(lints, "lints")) expect_length(lints, 3) } @@ -114,11 +114,11 @@ describe("robustness_lint_count derivation from mocked lints", { }) }) -describe("robustness_lint_count mock implementation", { +describe("correctness_lint_count mock implementation", { it("returns non-negative values", { results <- replicate(50, { p <- random_pkg() - p$robustness_lint_count + p$correctness_lint_count }) expect_true(all(results >= 0)) @@ -127,7 +127,7 @@ describe("robustness_lint_count mock implementation", { it("returns numeric values consistently", { results <- replicate(20, { p <- random_pkg() - p$robustness_lint_count + p$correctness_lint_count }) expect_true(all(vlapply(as.list(results), is.numeric))) From a7a7e4fb5af3e192ef52ce8a7f2f2feae2e06e3a Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 25 Feb 2026 12:40:03 +0100 Subject: [PATCH 06/22] docs: rebuild docs --- DESCRIPTION | 1 + man/metrics.Rd | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index ea63612..9132d4d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -87,6 +87,7 @@ Collate: 'generic_pkg_data_derive.R' 'impl_data.R' 'data_archive_md5.R' + 'data_correctness_lints.R' 'data_coverage.R' 'data_desc.R' 'data_documentation_examples.R' diff --git a/man/metrics.Rd b/man/metrics.Rd index 719958f..6f6dfca 100644 --- a/man/metrics.Rd +++ b/man/metrics.Rd @@ -27,10 +27,10 @@ For access to \emph{all} the internally calculated data, pass \code{all = TRUE}. } \keyword{workflow} \section{Metrics}{The following metrics are provided by \code{\link{val.meter}}. - \subsection{Robustness Lint Count}{ -\code{} Count of robustness lints identified by \pkg{lintr}. Robustness lints identify potentially error-prone code patterns based on relatively unopinionated good programming practices. Lower counts indicate more robust code. See \url{https://lintr.r-lib.org/reference/robustness_linters.html} + \subsection{Correctness Lint Count}{ +\code{} Count of correctness lints identified by \pkg{lintr}. Combines linters from the \emph{correctness}, \emph{common_mistakes}, and \emph{robustness} tags to identify potentially error-prone code patterns and common programming mistakes. Lower counts indicate higher code quality. See \url{https://lintr.r-lib.org/reference/correctness_linters.html} - \Sexpr[stage=render,results=rd]{if (length(find.package("lintr", quiet = TRUE)) > 0) "\\\\ifelse{html}{\\\\figure{badge-dep-lintr-x-flat-square-green.svg}{options: alt = \\"[lintr]\\"}}{\\\\strong{[dep::lintr]}}" else "\\\\ifelse{html}{\\\\figure{badge-dep-lintr-x-flat-square-red.svg}{options: alt = \\"[lintr]\\"}}{\\\\strong{[dep::lintr]}}"} + \Sexpr[stage=install,results=rd]{if (numeric_version(paste0(R.version$major, ".", R.version$minor)) < "4.5.0") { "\\\\ifelse{html}{\\\\figure{badge-best_practice-x-flat-square-blue.svg}{options: alt = \\"[best practice]\\"}}{\\\\strong{[best practice]}}" } else { "\\\\link[val.meter:tags]{\\\\ifelse{html}{\\\\figure{badge-best_practice-x-flat-square-blue.svg}{options: alt = \\"[best practice]\\"}}{\\\\strong{[best practice]}}}" }} } From 68b38697dea20b25d47fa1874a96c208b88cb4f0 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 17 Mar 2026 14:58:33 +0100 Subject: [PATCH 07/22] refactor: change correctness lints to overall lints --- R/data_correctness_lints.R | 75 -------------------- R/data_lints.R | 137 +++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 75 deletions(-) delete mode 100644 R/data_correctness_lints.R create mode 100644 R/data_lints.R diff --git a/R/data_correctness_lints.R b/R/data_correctness_lints.R deleted file mode 100644 index db80c71..0000000 --- a/R/data_correctness_lints.R +++ /dev/null @@ -1,75 +0,0 @@ -#' @include impl_data.R - -# Correctness lints metric implementation - -# Auxiliary data: full lint results -impl_data( - "correctness_lints", - class = S7::new_S3_class("lints"), - metric = FALSE, # Explicitly mark as auxiliary data, not a metric - tags = c("execution"), - suggests = "lintr", - permissions = "execution", - title = "Correctness Lint Results", - description = paste( - "Full \\pkg{lintr} correctness lint results for the package.", - "Contains detailed information about each lint including line numbers,", - "column positions, and lint messages. This is auxiliary data used to", - "derive the \\code{correctness_lint_count} metric." - ) -) - -impl_data( - "correctness_lints", - for_resource = source_code_resource, - function(pkg, resource, field, ..., quiet = opt("quiet")) { - wrapper <- if (quiet) { - function(...) capture.output(..., type = "message") - } else { - identity - } - - wrapper({ - result <- lintr::lint_package( - path = resource@path, - linters = lintr::linters_with_tags( - tags = c("correctness", "common_mistakes", "robustness") - ), - parse_settings = FALSE # Don't read .lintr config for consistency - ) - }) - - result - } -) - -# Primary metric: count of correctness lints -impl_data( - "correctness_lint_count", - metric = TRUE, - class = class_integer, - tags = c("best practice"), - title = "Correctness Lint Count", - description = paste( - "Count of correctness lints identified by \\pkg{lintr}.", - "Combines linters from the \\emph{correctness}, \\emph{common_mistakes},", - "and \\emph{robustness} tags to identify potentially error-prone code", - "patterns and common programming mistakes.", - "Lower counts indicate higher code quality.", - "See \\url{https://lintr.r-lib.org/reference/correctness_linters.html}" - ) -) - -impl_data( - "correctness_lint_count", - for_resource = source_code_resource, - function(pkg, resource, field, ...) { - length(pkg$correctness_lints) - } -) - -impl_data( - "correctness_lint_count", - for_resource = mock_resource, - function(pkg, resource, field, ...) rpois(1, 10) -) diff --git a/R/data_lints.R b/R/data_lints.R new file mode 100644 index 0000000..53cce1f --- /dev/null +++ b/R/data_lints.R @@ -0,0 +1,137 @@ +#' @include impl_data.R + +# Lint metric implementation + +# Helper: count total non-empty R source lines in a package +count_r_loc <- function(path) { + r_files <- list.files( + file.path(path, "R"), + pattern = "\\.R$", + full.names = TRUE, + ignore.case = TRUE + ) + if (length(r_files) == 0L) return(0L) + total <- 0L + for (f in r_files) { + lines <- readLines(f, warn = FALSE) + total <- total + sum(nzchar(trimws(lines))) + } + total +} + +# Helper: count unique (filename, line_number) pairs across all lints +count_linted_lines <- function(lints) { + if (length(lints) == 0L) return(0L) + pairs <- vcapply(lints, function(l) paste0(l$filename, ":", l$line_number)) + length(unique(pairs)) +} + +# Auxiliary data: full lint results +impl_data( + "lints", + class = S7::new_S3_class("lints"), + metric = FALSE, + tags = c("execution"), + suggests = "lintr", + permissions = "execution", + title = "Lint Results", + description = paste( + "Full \\pkg{lintr} lint results for the package.", + "Contains detailed information about each lint including line numbers,", + "column positions, and lint messages. This is auxiliary data used to", + "derive the \\code{lint_count} and \\code{lint_free_fraction} metrics.", + "The set of linters applied is controlled by the \\code{lint_linters} option." + ) +) + +impl_data( + "lints", + for_resource = source_code_resource, + function(pkg, resource, field, ..., quiet = opt("quiet")) { + linter_names <- opt("lint_linters") + linters <- setNames( + lapply(linter_names, function(nm) utils::getFromNamespace(nm, "lintr")()), + linter_names + ) + wrapper <- if (quiet) { + function(...) capture.output(..., type = "message") + } else { + identity + } + + wrapper({ + result <- lintr::lint_package( + path = resource@path, + linters = linters, + parse_settings = FALSE # Don't read .lintr config for consistency + ) + }) + + result + } +) + +# Primary metric: count of lints +impl_data( + "lint_count", + metric = TRUE, + class = class_integer, + tags = c("best practice"), + title = "Lint Count", + description = paste( + "Count of lints identified by \\pkg{lintr}.", + "The set of linters applied is controlled by the \\code{lint_linters} option,", + "which defaults to 26 linters from the \\emph{correctness},", + "\\emph{common_mistakes}, and \\emph{robustness} tags.", + "Lower counts indicate higher code quality." + ) +) + +impl_data( + "lint_count", + for_resource = source_code_resource, + function(pkg, resource, field, ...) { + length(pkg$lints) + } +) + +impl_data( + "lint_count", + for_resource = mock_resource, + function(pkg, resource, field, ...) rpois(1, 10) +) + +# Secondary metric: fraction of lintable lines that are lint-free +impl_data( + "lint_free_fraction", + metric = TRUE, + class = class_double, + tags = c("best practice"), + title = "Lint-Free Fraction", + description = paste( + "Fraction of non-empty R source lines that are free of lints,", + "as identified by \\pkg{lintr}. A value of 1 means no linted lines;", + "lower values indicate more pervasive lint issues.", + "The set of linters applied is controlled by the \\code{lint_linters} option.", + "Returns \\code{1} when the package has no R source lines." + ) +) + +impl_data( + "lint_free_fraction", + for_resource = source_code_resource, + function(pkg, resource, field, ...) { + loc <- count_r_loc(resource@path) + if (loc == 0L) return(1.0) + linted <- count_linted_lines(pkg$lints) + max(0.0, 1.0 - linted / loc) + } +) + +impl_data( + "lint_free_fraction", + for_resource = mock_resource, + function(pkg, resource, field, ...) { + runif(1, min = 0.7, max = 1.0) + } +) From 581bf89b2f9c598fc7b8506337a10cab632e1f7b Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 17 Mar 2026 14:58:47 +0100 Subject: [PATCH 08/22] feat: add option for configuring lints --- R/options.R | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/R/options.R b/R/options.R index 58e1bc9..d9d64fe 100644 --- a/R/options.R +++ b/R/options.R @@ -28,6 +28,42 @@ define_options( (for example, running `R CMD check`)", quiet = TRUE, + paste( + "Character vector of \\pkg{lintr} linter function names used when computing", + "\\code{lint_count} and \\code{lint_free_fraction}. Each name must correspond", + "to a zero-argument linter constructor exported by \\pkg{lintr} (e.g.", + "\\code{\"equals_na_linter\"}). Override to add, remove, or replace linters.", + "Note: linters requiring configuration (e.g. \\code{backport_linter}) are", + "excluded from the default set; add them manually with \\code{opt_set()} after", + "configuring via \\code{lintr::backport_linter(minimum_r_version = ...)}." + ), + lint_linters = c( + "absolute_path_linter", + "all_equal_linter", + "class_equals_linter", + "download_file_linter", + "equals_na_linter", + "for_loop_index_linter", + "length_test_linter", + "list_comparison_linter", + "missing_package_linter", + "namespace_linter", + "nonportable_path_linter", + "object_overwrite_linter", + "package_hooks_linter", + "pipe_return_linter", + "redundant_equals_linter", + "routine_registration_linter", + "sample_int_linter", + "seq_linter", + "sprintf_linter", + "strings_as_factors_linter", + "T_and_F_symbol_linter", + "terminal_close_linter", + "unused_import_linter", + "vector_logic_linter" + ), + fmt("Recognized source control hosting domains used when inferring whether a package has a source code repository on a recognized hosting platform. Customize this to add additional git hosting services (e.g., self-hosted From 7667d4b164c9cd917c04430ae6d23d67ad58be72 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 17 Mar 2026 14:58:56 +0100 Subject: [PATCH 09/22] test: update tests for lints --- tests/testthat/test-data_correctness_lints.R | 135 --------- tests/testthat/test-data_lints.R | 300 +++++++++++++++++++ 2 files changed, 300 insertions(+), 135 deletions(-) delete mode 100644 tests/testthat/test-data_correctness_lints.R create mode 100644 tests/testthat/test-data_lints.R diff --git a/tests/testthat/test-data_correctness_lints.R b/tests/testthat/test-data_correctness_lints.R deleted file mode 100644 index bf19398..0000000 --- a/tests/testthat/test-data_correctness_lints.R +++ /dev/null @@ -1,135 +0,0 @@ -# Helper: create a fake lintr `lints` object with n entries -create_mock_lints <- function(n = 3) { - lint_list <- replicate(n, structure( - list( - filename = "R/test.R", - line_number = 1L, - column_number = 1L, - type = "warning", - message = "mock correctness lint", - line = "x = 1", - ranges = NULL - ), - class = "lint" - ), simplify = FALSE) - - structure(lint_list, class = c("lints", "list")) -} - -# Helper: create a minimal package source directory for source_code_resource -create_mock_pkg_dir <- function(dir) { - dir.create(file.path(dir, "R"), recursive = TRUE) - writeLines( - c( - "Package: mockpkg", - "Version: 1.0.0", - "Title: Mock Package", - "Description: A mock package for testing.", - "Authors@R: person('A', 'B', role = 'aut')", - "License: MIT + file LICENSE" - ), - file.path(dir, "DESCRIPTION") - ) - writeLines("x <- 1", file.path(dir, "R", "mock.R")) - invisible(dir) -} - -describe("correctness_lint_count derivation from mocked lints", { - it("returns 0 when there are no lints", { - skip_if_not_installed("lintr") - tmp_dir <- withr::local_tempdir() - create_mock_pkg_dir(tmp_dir) - - with_mocked_bindings( - lint_package = function(...) create_mock_lints(0), - .package = "lintr", - { - p <- pkg( - source_code_resource(path = tmp_dir, package = "mockpkg"), - permissions = permissions("execution") - ) - result <- p$correctness_lint_count - expect_equal(result, 0L) - } - ) - }) - - it("returns the correct count matching number of lints", { - skip_if_not_installed("lintr") - tmp_dir <- withr::local_tempdir() - create_mock_pkg_dir(tmp_dir) - - with_mocked_bindings( - lint_package = function(...) create_mock_lints(7), - .package = "lintr", - { - p <- pkg( - source_code_resource(path = tmp_dir, package = "mockpkg"), - permissions = permissions("execution") - ) - result <- p$correctness_lint_count - expect_equal(result, 7L) - } - ) - }) - - it("returns a non-negative integer", { - skip_if_not_installed("lintr") - tmp_dir <- withr::local_tempdir() - create_mock_pkg_dir(tmp_dir) - - with_mocked_bindings( - lint_package = function(...) create_mock_lints(4), - .package = "lintr", - { - p <- pkg( - source_code_resource(path = tmp_dir, package = "mockpkg"), - permissions = permissions("execution") - ) - result <- p$correctness_lint_count - expect_true(is.numeric(result)) - expect_true(result >= 0) - } - ) - }) - - it("stores the full lints object in correctness_lints", { - skip_if_not_installed("lintr") - tmp_dir <- withr::local_tempdir() - create_mock_pkg_dir(tmp_dir) - - with_mocked_bindings( - lint_package = function(...) create_mock_lints(3), - .package = "lintr", - { - p <- pkg( - source_code_resource(path = tmp_dir, package = "mockpkg"), - permissions = permissions("execution") - ) - lints <- p$correctness_lints - expect_true(inherits(lints, "lints")) - expect_length(lints, 3) - } - ) - }) -}) - -describe("correctness_lint_count mock implementation", { - it("returns non-negative values", { - results <- replicate(50, { - p <- random_pkg() - p$correctness_lint_count - }) - - expect_true(all(results >= 0)) - }) - - it("returns numeric values consistently", { - results <- replicate(20, { - p <- random_pkg() - p$correctness_lint_count - }) - - expect_true(all(vlapply(as.list(results), is.numeric))) - }) -}) diff --git a/tests/testthat/test-data_lints.R b/tests/testthat/test-data_lints.R new file mode 100644 index 0000000..47d9957 --- /dev/null +++ b/tests/testthat/test-data_lints.R @@ -0,0 +1,300 @@ +# Helper: create a fake lintr `lints` object with n entries +create_mock_lints <- function(n = 3) { + lint_list <- replicate(n, structure( + list( + filename = "R/test.R", + line_number = 1L, + column_number = 1L, + type = "warning", + message = "mock lint", + line = "x = 1", + ranges = NULL + ), + class = "lint" + ), simplify = FALSE) + + structure(lint_list, class = c("lints", "list")) +} + +# Helper: create a minimal package source directory for source_code_resource +create_mock_pkg_dir <- function(dir) { + dir.create(file.path(dir, "R"), recursive = TRUE) + writeLines( + c( + "Package: mockpkg", + "Version: 1.0.0", + "Title: Mock Package", + "Description: A mock package for testing.", + "Authors@R: person('A', 'B', role = 'aut')", + "License: MIT + file LICENSE" + ), + file.path(dir, "DESCRIPTION") + ) + writeLines("x <- 1", file.path(dir, "R", "mock.R")) + invisible(dir) +} + +describe("lint_count derivation from mocked lints", { + it("returns 0 when there are no lints", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(0), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + result <- p$lint_count + expect_equal(result, 0L) + } + ) + }) + + it("returns the correct count matching number of lints", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(7), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + result <- p$lint_count + expect_equal(result, 7L) + } + ) + }) + + it("returns a non-negative integer", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(4), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + result <- p$lint_count + expect_true(is.numeric(result)) + expect_true(result >= 0) + } + ) + }) + + it("stores the full lints object in lints", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(3), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + lints <- p$lints + expect_true(inherits(lints, "lints")) + expect_length(lints, 3) + } + ) + }) +}) + +describe("lint_count mock implementation", { + it("returns non-negative values", { + results <- replicate(50, { + p <- random_pkg() + p$lint_count + }) + + expect_true(all(results >= 0)) + }) + + it("returns numeric values consistently", { + results <- replicate(20, { + p <- random_pkg() + p$lint_count + }) + + expect_true(all(vlapply(as.list(results), is.numeric))) + }) +}) + +describe("count_r_loc helper", { + it("returns 0 for a package with no R files", { + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "R")) + result <- val.meter:::count_r_loc(tmp_dir) + expect_equal(result, 0L) + }) + + it("counts non-empty lines across R files", { + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "R")) + writeLines(c("x <- 1", "", "y <- 2"), file.path(tmp_dir, "R", "a.R")) + writeLines(c("z <- 3"), file.path(tmp_dir, "R", "b.R")) + result <- val.meter:::count_r_loc(tmp_dir) + expect_equal(result, 3L) + }) +}) + +describe("count_linted_lines helper", { + it("returns 0 for empty lints", { + result <- val.meter:::count_linted_lines(create_mock_lints(0)) + expect_equal(result, 0L) + }) + + it("deduplicates lines with multiple lints", { + lints_same_line <- structure( + list( + structure( + list(filename = "R/a.R", line_number = 5L, column_number = 1L, + type = "warning", message = "lint 1", line = "x", ranges = NULL), + class = "lint" + ), + structure( + list(filename = "R/a.R", line_number = 5L, column_number = 3L, + type = "warning", message = "lint 2", line = "x", ranges = NULL), + class = "lint" + ), + structure( + list(filename = "R/a.R", line_number = 6L, column_number = 1L, + type = "warning", message = "lint 3", line = "y", ranges = NULL), + class = "lint" + ) + ), + class = c("lints", "list") + ) + result <- val.meter:::count_linted_lines(lints_same_line) + expect_equal(result, 2L) + }) +}) + +describe("lint_free_fraction derivation", { + it("returns 1 when there are no lints", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(0), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + result <- p$lint_free_fraction + expect_equal(result, 1.0) + } + ) + }) + + it("returns a value in [0, 1]", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + with_mocked_bindings( + lint_package = function(...) create_mock_lints(2), + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + result <- p$lint_free_fraction + expect_true(is.numeric(result)) + expect_true(result >= 0.0 && result <= 1.0) + } + ) + }) +}) + +describe("lint_free_fraction mock implementation", { + it("returns values in [0, 1]", { + results <- replicate(50, { + p <- random_pkg() + p$lint_free_fraction + }) + + expect_true(all(results >= 0.0 & results <= 1.0)) + }) + + it("returns numeric values consistently", { + results <- replicate(20, { + p <- random_pkg() + p$lint_free_fraction + }) + + expect_true(all(vlapply(as.list(results), is.numeric))) + }) +}) + +describe("lint_linters option customization", { + it("uses the linter names from the option", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + create_mock_pkg_dir(tmp_dir) + + called_with <- NULL + old <- opt_set("lint_linters", c("equals_na_linter", "seq_linter")) + on.exit(opt_set("lint_linters", old)) + + with_mocked_bindings( + lint_package = function(..., linters) { + called_with <<- names(linters) + create_mock_lints(0) + }, + .package = "lintr", + { + p <- pkg( + source_code_resource(path = tmp_dir, package = "mockpkg"), + permissions = permissions("execution") + ) + p$lints + } + ) + + expect_equal(sort(called_with), c("equals_na_linter", "seq_linter")) + }) + + it("can restrict to a subset of linters", { + old <- opt_set("lint_linters", c("equals_na_linter")) + on.exit(opt_set("lint_linters", old)) + + linters <- opt("lint_linters") + expect_equal(linters, "equals_na_linter") + expect_length(linters, 1L) + }) + + it("default option contains expected linter names", { + linters <- opt("lint_linters") + + expect_true("equals_na_linter" %in% linters) + expect_true("seq_linter" %in% linters) + expect_true("vector_logic_linter" %in% linters) + expect_true("T_and_F_symbol_linter" %in% linters) + expect_equal(length(linters), 26L) + + # Excluded linters should not be present + expect_false("backport_linter" %in% linters) + expect_false("undesirable_function_linter" %in% linters) + expect_false("undesirable_operator_linter" %in% linters) + expect_false("sample_int_linter" %in% linters) + }) +}) From 61ca4f7aba93c6fde8bc2ca3d50300f6198a2cfb Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 17 Mar 2026 15:00:44 +0100 Subject: [PATCH 10/22] docs: recompile documentation --- DESCRIPTION | 2 +- man/metrics.Rd | 11 +++++++++-- man/options.Rd | 13 +++++++++++++ man/options_params.Rd | 2 ++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9132d4d..3d38772 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -87,12 +87,12 @@ Collate: 'generic_pkg_data_derive.R' 'impl_data.R' 'data_archive_md5.R' - 'data_correctness_lints.R' 'data_coverage.R' 'data_desc.R' 'data_documentation_examples.R' 'data_downloads_total.R' 'data_has_current_news.R' + 'data_lints.R' 'data_r_cmd_check.R' 'data_recognized_source.R' 'data_reverse_dependencies.R' diff --git a/man/metrics.Rd b/man/metrics.Rd index 6f6dfca..c612914 100644 --- a/man/metrics.Rd +++ b/man/metrics.Rd @@ -27,8 +27,8 @@ For access to \emph{all} the internally calculated data, pass \code{all = TRUE}. } \keyword{workflow} \section{Metrics}{The following metrics are provided by \code{\link{val.meter}}. - \subsection{Correctness Lint Count}{ -\code{} Count of correctness lints identified by \pkg{lintr}. Combines linters from the \emph{correctness}, \emph{common_mistakes}, and \emph{robustness} tags to identify potentially error-prone code patterns and common programming mistakes. Lower counts indicate higher code quality. See \url{https://lintr.r-lib.org/reference/correctness_linters.html} + \subsection{Lint Count}{ +\code{} Count of lints identified by \pkg{lintr}. The set of linters applied is controlled by the \code{lint_linters} option, which defaults to 26 linters from the \emph{correctness}, \emph{common_mistakes}, and \emph{robustness} tags. Lower counts indicate higher code quality. @@ -40,6 +40,13 @@ For access to \emph{all} the internally calculated data, pass \code{all = TRUE}. +} + \subsection{Lint-Free Fraction}{ +\code{} Fraction of non-empty R source lines that are free of lints, as identified by \pkg{lintr}. A value of 1 means no linted lines; lower values indicate more pervasive lint issues. The set of linters applied is controlled by the \code{lint_linters} option. Returns \code{1} when the package has no R source lines. + + + +\Sexpr[stage=install,results=rd]{if (numeric_version(paste0(R.version$major, ".", R.version$minor)) < "4.5.0") { "\\\\ifelse{html}{\\\\figure{badge-best_practice-x-flat-square-blue.svg}{options: alt = \\"[best practice]\\"}}{\\\\strong{[best practice]}}" } else { "\\\\link[val.meter:tags]{\\\\ifelse{html}{\\\\figure{badge-best_practice-x-flat-square-blue.svg}{options: alt = \\"[best practice]\\"}}{\\\\strong{[best practice]}}}" }} } \subsection{CRAN Reverse Dependencies Count}{ \code{} The number of packages on \acronym{CRAN} that directly depend on this package through \code{Depends}, \code{Imports}, or \code{LinkingTo} fields. This metric reflects adoption within the CRAN ecosystem and indicates how many packages would be affected by breaking changes. Higher counts suggest wider usage and community trust, but also greater responsibility for maintaining backward compatibility. diff --git a/man/options.Rd b/man/options.Rd index b861199..dce567a 100644 --- a/man/options.Rd +++ b/man/options.Rd @@ -51,6 +51,19 @@ resources (such as download and installation output) and executing code \item{envvar: }{R_VAL_METER_QUIET (evaluated if possible, raw string otherwise)} }} +\item{lint_linters}{\describe{ +Character vector of \pkg{lintr} linter function names used when computing \code{lint_count} and \code{lint_free_fraction}. Each name must correspond to a zero-argument linter constructor exported by \pkg{lintr} (e.g. \code{"equals_na_linter"}). Override to add, remove, or replace linters. Note: linters requiring configuration (e.g. \code{backport_linter}) are excluded from the default set; add them manually with \code{opt_set()} after configuring via \code{lintr::backport_linter(minimum_r_version = ...)}.\item{default: }{\preformatted{c("absolute_path_linter", "all_equal_linter", "class_equals_linter", + "download_file_linter", "equals_na_linter", "for_loop_index_linter", + "length_test_linter", "list_comparison_linter", "missing_package_linter", + "namespace_linter", "nonportable_path_linter", "object_overwrite_linter", + "package_hooks_linter", "pipe_return_linter", "redundant_equals_linter", + "routine_registration_linter", "sample_int_linter", "seq_linter", + "sprintf_linter", "strings_as_factors_linter", "T_and_F_symbol_linter", + "terminal_close_linter", "unused_import_linter", "vector_logic_linter")}} +\item{option: }{val.meter.lint_linters} +\item{envvar: }{R_VAL_METER_LINT_LINTERS (evaluated if possible, raw string otherwise)} +}} + \item{source_control_domains}{\describe{ Recognized source control hosting domains used when inferring whether a package has a source code repository on a recognized hosting platform. diff --git a/man/options_params.Rd b/man/options_params.Rd index 5e588b2..8f78c56 100644 --- a/man/options_params.Rd +++ b/man/options_params.Rd @@ -4,6 +4,8 @@ \alias{options_params} \title{Options As Parameters} \arguments{ +\item{lint_linters}{Character vector of \pkg{lintr} linter function names used when computing \code{lint_count} and \code{lint_free_fraction}. Each name must correspond to a zero-argument linter constructor exported by \pkg{lintr} (e.g. \code{"equals_na_linter"}). Override to add, remove, or replace linters. Note: linters requiring configuration (e.g. \code{backport_linter}) are excluded from the default set; add them manually with \code{opt_set()} after configuring via \code{lintr::backport_linter(minimum_r_version = ...)}. (Defaults to \verb{c("absolute_path_linter", "all_equal_linter", "class_equals_linter", ; "download_file_linter", "equals_na_linter", "for_loop_index_linter", ; "length_test_linter", "list_comparison_linter", "missing_package_linter", ; "namespace_linter", "nonportable_path_linter", "object_overwrite_linter", ; "package_hooks_linter", "pipe_return_linter", "redundant_equals_linter", ; "routine_registration_linter", "sample_int_linter", "seq_linter", ; "sprintf_linter", "strings_as_factors_linter", "T_and_F_symbol_linter", ; "terminal_close_linter", "unused_import_linter", "vector_logic_linter")}, overwritable using option 'val.meter.lint_linters' or environment variable 'R_VAL_METER_LINT_LINTERS')} + \item{tags}{Set the default \code{val.meter} tags policy. Tags characterize the types of information various metrics contain. For more details, see \code{\link[=tags]{tags()}}. (Defaults to \code{tags(TRUE)}, overwritable using option 'val.meter.tags' or environment variable 'R_VAL_METER_TAGS')} From b2dcd9e031550e9be4cb1605063cefc5791abe12 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 17 Mar 2026 15:14:07 +0100 Subject: [PATCH 11/22] feat: remove namespace and missing package linters --- R/options.R | 2 -- 1 file changed, 2 deletions(-) diff --git a/R/options.R b/R/options.R index d9d64fe..b4f449a 100644 --- a/R/options.R +++ b/R/options.R @@ -46,8 +46,6 @@ define_options( "for_loop_index_linter", "length_test_linter", "list_comparison_linter", - "missing_package_linter", - "namespace_linter", "nonportable_path_linter", "object_overwrite_linter", "package_hooks_linter", From b2b8c99e0969e2ef7ea2546d47850a15fa36ea4f Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 17 Mar 2026 15:18:51 +0100 Subject: [PATCH 12/22] test: remove unneeded test --- tests/testthat/test-data_lints.R | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/testthat/test-data_lints.R b/tests/testthat/test-data_lints.R index 47d9957..6a0b786 100644 --- a/tests/testthat/test-data_lints.R +++ b/tests/testthat/test-data_lints.R @@ -281,20 +281,4 @@ describe("lint_linters option customization", { expect_equal(linters, "equals_na_linter") expect_length(linters, 1L) }) - - it("default option contains expected linter names", { - linters <- opt("lint_linters") - - expect_true("equals_na_linter" %in% linters) - expect_true("seq_linter" %in% linters) - expect_true("vector_logic_linter" %in% linters) - expect_true("T_and_F_symbol_linter" %in% linters) - expect_equal(length(linters), 26L) - - # Excluded linters should not be present - expect_false("backport_linter" %in% linters) - expect_false("undesirable_function_linter" %in% linters) - expect_false("undesirable_operator_linter" %in% linters) - expect_false("sample_int_linter" %in% linters) - }) }) From 1336cfa5de912b9012be0116168653be7a23811e Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Tue, 17 Mar 2026 15:56:52 +0100 Subject: [PATCH 13/22] docs: update lints and opts documentation --- R/data_lints.R | 14 ++++++++------ R/options.R | 17 +++++++++-------- man/metrics.Rd | 2 +- man/options.Rd | 21 ++++++++++++++------- man/options_params.Rd | 9 ++++++++- 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/R/data_lints.R b/R/data_lints.R index 53cce1f..3d52d88 100644 --- a/R/data_lints.R +++ b/R/data_lints.R @@ -40,7 +40,8 @@ impl_data( "Contains detailed information about each lint including line numbers,", "column positions, and lint messages. This is auxiliary data used to", "derive the \\code{lint_count} and \\code{lint_free_fraction} metrics.", - "The set of linters applied is controlled by the \\code{lint_linters} option." + "The set of linters applied is controlled by the \\code{lint_linters}", + "option." ) ) @@ -80,9 +81,10 @@ impl_data( title = "Lint Count", description = paste( "Count of lints identified by \\pkg{lintr}.", - "The set of linters applied is controlled by the \\code{lint_linters} option,", - "which defaults to 26 linters from the \\emph{correctness},", - "\\emph{common_mistakes}, and \\emph{robustness} tags.", + "The set of linters applied is controlled by the \\code{lint_linters}", + "option, which defaults to linters from the \\emph{correctness},", + "\\emph{common_mistakes}, and \\emph{robustness} tags excluding", + "those that require configuration or are environment-dependent.", "Lower counts indicate higher code quality." ) ) @@ -112,8 +114,8 @@ impl_data( "Fraction of non-empty R source lines that are free of lints,", "as identified by \\pkg{lintr}. A value of 1 means no linted lines;", "lower values indicate more pervasive lint issues.", - "The set of linters applied is controlled by the \\code{lint_linters} option.", - "Returns \\code{1} when the package has no R source lines." + "The set of linters applied is controlled by the \\code{lint_linters}", + "option. Returns \\code{1} when the package has no R source lines." ) ) diff --git a/R/options.R b/R/options.R index b4f449a..863ad49 100644 --- a/R/options.R +++ b/R/options.R @@ -28,14 +28,15 @@ define_options( (for example, running `R CMD check`)", quiet = TRUE, - paste( - "Character vector of \\pkg{lintr} linter function names used when computing", - "\\code{lint_count} and \\code{lint_free_fraction}. Each name must correspond", - "to a zero-argument linter constructor exported by \\pkg{lintr} (e.g.", - "\\code{\"equals_na_linter\"}). Override to add, remove, or replace linters.", - "Note: linters requiring configuration (e.g. \\code{backport_linter}) are", - "excluded from the default set; add them manually with \\code{opt_set()} after", - "configuring via \\code{lintr::backport_linter(minimum_r_version = ...)}." + fmt( + "Character vector of `{{lintr}}` linter function names used when computing + `lint_count` and `lint_free_fraction`. Each name must correspond + to a linter constructor exported by `{{lintr}}` (e.g. + `\"equals_na_linter\"`. Override to add, remove, or replace linters. + Note: linters requiring configuration or environment-specific ones + (e.g. `\"backport_linter\"`) are excluded from the default set; + add them manually by changing this option, possibly after configuring + them globally." ), lint_linters = c( "absolute_path_linter", diff --git a/man/metrics.Rd b/man/metrics.Rd index c612914..56f6abb 100644 --- a/man/metrics.Rd +++ b/man/metrics.Rd @@ -28,7 +28,7 @@ For access to \emph{all} the internally calculated data, pass \code{all = TRUE}. \keyword{workflow} \section{Metrics}{The following metrics are provided by \code{\link{val.meter}}. \subsection{Lint Count}{ -\code{} Count of lints identified by \pkg{lintr}. The set of linters applied is controlled by the \code{lint_linters} option, which defaults to 26 linters from the \emph{correctness}, \emph{common_mistakes}, and \emph{robustness} tags. Lower counts indicate higher code quality. +\code{} Count of lints identified by \pkg{lintr}. The set of linters applied is controlled by the \code{lint_linters} option, which defaults to linters from the \emph{correctness}, \emph{common_mistakes}, and \emph{robustness} tags excluding those that require configuration or are environment-dependent. Lower counts indicate higher code quality. diff --git a/man/options.Rd b/man/options.Rd index dce567a..de7436b 100644 --- a/man/options.Rd +++ b/man/options.Rd @@ -52,14 +52,21 @@ resources (such as download and installation output) and executing code }} \item{lint_linters}{\describe{ -Character vector of \pkg{lintr} linter function names used when computing \code{lint_count} and \code{lint_free_fraction}. Each name must correspond to a zero-argument linter constructor exported by \pkg{lintr} (e.g. \code{"equals_na_linter"}). Override to add, remove, or replace linters. Note: linters requiring configuration (e.g. \code{backport_linter}) are excluded from the default set; add them manually with \code{opt_set()} after configuring via \code{lintr::backport_linter(minimum_r_version = ...)}.\item{default: }{\preformatted{c("absolute_path_linter", "all_equal_linter", "class_equals_linter", +Character vector of \code{{lintr}} linter function names used when computing +\code{lint_count} and \code{lint_free_fraction}. Each name must correspond +to a linter constructor exported by \code{{lintr}} (e.g. +\code{"equals_na_linter"}. Override to add, remove, or replace linters. +Note: linters requiring configuration or environment-specific ones +(e.g. \code{"backport_linter"}) are excluded from the default set; +add them manually by changing this option, possibly after configuring +them globally.\item{default: }{\preformatted{c("absolute_path_linter", "all_equal_linter", "class_equals_linter", "download_file_linter", "equals_na_linter", "for_loop_index_linter", - "length_test_linter", "list_comparison_linter", "missing_package_linter", - "namespace_linter", "nonportable_path_linter", "object_overwrite_linter", - "package_hooks_linter", "pipe_return_linter", "redundant_equals_linter", - "routine_registration_linter", "sample_int_linter", "seq_linter", - "sprintf_linter", "strings_as_factors_linter", "T_and_F_symbol_linter", - "terminal_close_linter", "unused_import_linter", "vector_logic_linter")}} + "length_test_linter", "list_comparison_linter", "nonportable_path_linter", + "object_overwrite_linter", "package_hooks_linter", "pipe_return_linter", + "redundant_equals_linter", "routine_registration_linter", + "sample_int_linter", "seq_linter", "sprintf_linter", "strings_as_factors_linter", + "T_and_F_symbol_linter", "terminal_close_linter", "unused_import_linter", + "vector_logic_linter")}} \item{option: }{val.meter.lint_linters} \item{envvar: }{R_VAL_METER_LINT_LINTERS (evaluated if possible, raw string otherwise)} }} diff --git a/man/options_params.Rd b/man/options_params.Rd index 8f78c56..c3a9f18 100644 --- a/man/options_params.Rd +++ b/man/options_params.Rd @@ -4,7 +4,14 @@ \alias{options_params} \title{Options As Parameters} \arguments{ -\item{lint_linters}{Character vector of \pkg{lintr} linter function names used when computing \code{lint_count} and \code{lint_free_fraction}. Each name must correspond to a zero-argument linter constructor exported by \pkg{lintr} (e.g. \code{"equals_na_linter"}). Override to add, remove, or replace linters. Note: linters requiring configuration (e.g. \code{backport_linter}) are excluded from the default set; add them manually with \code{opt_set()} after configuring via \code{lintr::backport_linter(minimum_r_version = ...)}. (Defaults to \verb{c("absolute_path_linter", "all_equal_linter", "class_equals_linter", ; "download_file_linter", "equals_na_linter", "for_loop_index_linter", ; "length_test_linter", "list_comparison_linter", "missing_package_linter", ; "namespace_linter", "nonportable_path_linter", "object_overwrite_linter", ; "package_hooks_linter", "pipe_return_linter", "redundant_equals_linter", ; "routine_registration_linter", "sample_int_linter", "seq_linter", ; "sprintf_linter", "strings_as_factors_linter", "T_and_F_symbol_linter", ; "terminal_close_linter", "unused_import_linter", "vector_logic_linter")}, overwritable using option 'val.meter.lint_linters' or environment variable 'R_VAL_METER_LINT_LINTERS')} +\item{lint_linters}{Character vector of \code{{lintr}} linter function names used when computing +\code{lint_count} and \code{lint_free_fraction}. Each name must correspond +to a linter constructor exported by \code{{lintr}} (e.g. +\code{"equals_na_linter"}. Override to add, remove, or replace linters. +Note: linters requiring configuration or environment-specific ones +(e.g. \code{"backport_linter"}) are excluded from the default set; +add them manually by changing this option, possibly after configuring +them globally. (Defaults to \verb{c("absolute_path_linter", "all_equal_linter", "class_equals_linter", ; "download_file_linter", "equals_na_linter", "for_loop_index_linter", ; "length_test_linter", "list_comparison_linter", "nonportable_path_linter", ; "object_overwrite_linter", "package_hooks_linter", "pipe_return_linter", ; "redundant_equals_linter", "routine_registration_linter", ; "sample_int_linter", "seq_linter", "sprintf_linter", "strings_as_factors_linter", ; "T_and_F_symbol_linter", "terminal_close_linter", "unused_import_linter", ; "vector_logic_linter")}, overwritable using option 'val.meter.lint_linters' or environment variable 'R_VAL_METER_LINT_LINTERS')} \item{tags}{Set the default \code{val.meter} tags policy. Tags characterize the types of information various metrics contain. For more details, see From a21bfaf0d7ee2fecf8beae0acc775afe798b8226 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 18 Mar 2026 13:05:04 +0100 Subject: [PATCH 14/22] feat: improve the way of counting R lines --- R/data_lints.R | 89 +++++++++++++++++++++++++------- tests/testthat/test-data_lints.R | 38 ++++++++++++-- 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/R/data_lints.R b/R/data_lints.R index 3d52d88..1fe2ce5 100644 --- a/R/data_lints.R +++ b/R/data_lints.R @@ -2,21 +2,69 @@ # Lint metric implementation -# Helper: count total non-empty R source lines in a package -count_r_loc <- function(path) { - r_files <- list.files( - file.path(path, "R"), - pattern = "\\.R$", - full.names = TRUE, - ignore.case = TRUE - ) +#' Count token-bearing, non-comment lines in files that lintr processes +#' +#' Uses lintr's own parsing frontend to ensure the denominator matches +#' the scope of lint_package(). A "relevant code line" is a physical +#' line containing at least one terminal token that is not a comment. +#' +#' This approach: +#' - Matches lint_package() scope: R/, tests/, inst/, vignettes/, data-raw/, +#' demo/, exec/ +#' - Matches lint_package() file pattern: .R, .Rmd, .qmd, .Rnw, .Rhtml, .Rrst, +#' .Rtex, .Rtxt +#' - Excludes blank lines and comment-only lines via parse data +#' - Correctly handles # inside strings (uses R's parser, not regex) +#' +#' @param path Path to package source directory +#' @return Integer count of code lines +#' @noRd +count_lintable_code_lines <- function(path) { + # Same directories as lint_package() + pkg_dirs <- c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec") + + # Same file pattern as lint_dir() default (used by lint_package) + file_pattern <- "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" + + # Find all matching files in package directories + r_files <- character(0) + for (dir in pkg_dirs) { + dir_path <- file.path(path, dir) + if (dir.exists(dir_path)) { + files <- list.files( + dir_path, + pattern = file_pattern, + recursive = TRUE, + full.names = TRUE + ) + r_files <- c(r_files, files) + } + } + if (length(r_files) == 0L) return(0L) - total <- 0L - for (f in r_files) { - lines <- readLines(f, warn = FALSE) - total <- total + sum(nzchar(trimws(lines))) + + total_lines <- 0L + + for (file in r_files) { + tryCatch({ + se <- lintr::get_source_expressions(file) + + # Last element contains full file parse data + full_expr <- se$expressions[[length(se$expressions)]] + pd <- full_expr$full_parsed_content + + if (!is.null(pd) && nrow(pd) > 0L) { + # Count lines with terminal tokens that are not comments + code_lines <- unique(pd$line1[pd$terminal & pd$token != "COMMENT"]) + total_lines <- total_lines + length(code_lines) + } + }, error = function(e) { + # Skip unparseable files (lintr would skip them too) + NULL + }) } - total + + total_lines } # Helper: count unique (filename, line_number) pairs across all lints @@ -111,11 +159,14 @@ impl_data( tags = c("best practice"), title = "Lint-Free Fraction", description = paste( - "Fraction of non-empty R source lines that are free of lints,", - "as identified by \\pkg{lintr}. A value of 1 means no linted lines;", - "lower values indicate more pervasive lint issues.", - "The set of linters applied is controlled by the \\code{lint_linters}", - "option. Returns \\code{1} when the package has no R source lines." + "Fraction of R code lines that are free of lints.", + "Code lines are defined as lines containing executable R tokens", + "(excluding comments and blank lines) across the standard package", + "directories (\\code{R/}, \\code{tests/}, \\code{inst/},", + "\\code{vignettes/}, \\code{data-raw/}, \\code{demo/}, \\code{exec/}).", + "A value of 1 means no linted lines; lower values indicate more", + "pervasive lint issues. The set of linters applied is controlled by", + "the \\code{lint_linters} option." ) ) @@ -123,7 +174,7 @@ impl_data( "lint_free_fraction", for_resource = source_code_resource, function(pkg, resource, field, ...) { - loc <- count_r_loc(resource@path) + loc <- count_lintable_code_lines(resource@path) if (loc == 0L) return(1.0) linted <- count_linted_lines(pkg$lints) max(0.0, 1.0 - linted / loc) diff --git a/tests/testthat/test-data_lints.R b/tests/testthat/test-data_lints.R index 6a0b786..53046e7 100644 --- a/tests/testthat/test-data_lints.R +++ b/tests/testthat/test-data_lints.R @@ -134,22 +134,52 @@ describe("lint_count mock implementation", { }) }) -describe("count_r_loc helper", { +describe("count_lintable_code_lines helper", { it("returns 0 for a package with no R files", { + skip_if_not_installed("lintr") tmp_dir <- withr::local_tempdir() dir.create(file.path(tmp_dir, "R")) - result <- val.meter:::count_r_loc(tmp_dir) + result <- val.meter:::count_lintable_code_lines(tmp_dir) expect_equal(result, 0L) }) - it("counts non-empty lines across R files", { + it("counts token-bearing non-comment lines across R files", { + skip_if_not_installed("lintr") tmp_dir <- withr::local_tempdir() dir.create(file.path(tmp_dir, "R")) + # Line 1: code, Line 2: blank (excluded), Line 3: code writeLines(c("x <- 1", "", "y <- 2"), file.path(tmp_dir, "R", "a.R")) + # Line 1: code writeLines(c("z <- 3"), file.path(tmp_dir, "R", "b.R")) - result <- val.meter:::count_r_loc(tmp_dir) + result <- val.meter:::count_lintable_code_lines(tmp_dir) expect_equal(result, 3L) }) + + it("excludes comment-only lines", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "R")) + # 2 code lines, 2 comment lines, 1 blank + writeLines( + c("# This is a comment", "x <- 1", "", "#' roxygen", "y <- 2"), + file.path(tmp_dir, "R", "a.R") + ) + result <- val.meter:::count_lintable_code_lines(tmp_dir) + expect_equal(result, 2L) + }) + + it("includes tests/ directory in scope", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "R")) + dir.create(file.path(tmp_dir, "tests", "testthat"), recursive = TRUE) + writeLines("x <- 1", file.path(tmp_dir, "R", "a.R")) + writeLines("test_that('works', { expect_true(TRUE) })", + file.path(tmp_dir, "tests", "testthat", "test-a.R")) + result <- val.meter:::count_lintable_code_lines(tmp_dir) + # 1 from R/, 1 from tests/ + expect_equal(result, 2L) + }) }) describe("count_linted_lines helper", { From 4e9e42dd6fcd6d8fa743e2add08559a54c19b2af Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 18 Mar 2026 13:14:35 +0100 Subject: [PATCH 15/22] feat: make pkg dirs and pattern a parameter --- R/data_lints.R | 125 ++++++++++++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/R/data_lints.R b/R/data_lints.R index 1fe2ce5..8684c1e 100644 --- a/R/data_lints.R +++ b/R/data_lints.R @@ -2,71 +2,78 @@ # Lint metric implementation -#' Count token-bearing, non-comment lines in files that lintr processes -#' -#' Uses lintr's own parsing frontend to ensure the denominator matches -#' the scope of lint_package(). A "relevant code line" is a physical -#' line containing at least one terminal token that is not a comment. -#' -#' This approach: -#' - Matches lint_package() scope: R/, tests/, inst/, vignettes/, data-raw/, -#' demo/, exec/ -#' - Matches lint_package() file pattern: .R, .Rmd, .qmd, .Rnw, .Rhtml, .Rrst, -#' .Rtex, .Rtxt -#' - Excludes blank lines and comment-only lines via parse data -#' - Correctly handles # inside strings (uses R's parser, not regex) -#' -#' @param path Path to package source directory -#' @return Integer count of code lines -#' @noRd -count_lintable_code_lines <- function(path) { - # Same directories as lint_package() - pkg_dirs <- c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec") - - # Same file pattern as lint_dir() default (used by lint_package) - file_pattern <- "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" - - # Find all matching files in package directories - r_files <- character(0) - for (dir in pkg_dirs) { - dir_path <- file.path(path, dir) - if (dir.exists(dir_path)) { - files <- list.files( - dir_path, - pattern = file_pattern, - recursive = TRUE, - full.names = TRUE - ) - r_files <- c(r_files, files) + #' Count token-bearing, non-comment lines in files that lintr processes + #' + #' Uses lintr's own parsing frontend to ensure the denominator matches + #' the scope of lint_package(). A "relevant code line" is a physical + #' line containing at least one terminal token that is not a comment. + #' + #' This approach: + #' - Matches lint_package() scope by default (configurable via pkg_dirs) + #' - Matches lint_package() file pattern by default (configurable via + #' file_pattern) + #' - Excludes blank lines and comment-only lines via parse data + #' - Correctly handles # inside strings (uses R's parser, not regex) + #' + #' @param path Path to package source directory. + #' @param pkg_dirs Character vector of subdirectories to search. Defaults to + #' the same directories as lint_package(): R/, tests/, inst/, vignettes/, + #' data-raw/, demo/, exec/. + #' @param file_pattern Regex pattern for file extensions. Defaults to the same + #' pattern as lint_dir(): .R, .Rmd, .qmd, .Rnw, .Rhtml, .Rrst, .Rtex, .Rtxt. + #' @return Integer count of code lines. + #' @noRd + count_lintable_code_lines <- function( + path, + pkg_dirs = c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec"), + file_pattern = "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" + ) { + # Find all matching files in package directories + r_files <- character(0) + for (dir in pkg_dirs) { + dir_path <- file.path(path, dir) + if (dir.exists(dir_path)) { + files <- list.files( + dir_path, + pattern = file_pattern, + recursive = TRUE, + full.names = TRUE + ) + r_files <- c(r_files, files) + } } - } - if (length(r_files) == 0L) return(0L) - - total_lines <- 0L - - for (file in r_files) { - tryCatch({ - se <- lintr::get_source_expressions(file) + if (length(r_files) == 0L) { + return(0L) + } - # Last element contains full file parse data - full_expr <- se$expressions[[length(se$expressions)]] - pd <- full_expr$full_parsed_content + total_lines <- 0L + + for (file in r_files) { + tryCatch( + { + se <- lintr::get_source_expressions(file) + + # Last element contains full file parse data + full_expr <- se$expressions[[length(se$expressions)]] + pd <- full_expr$full_parsed_content + + if (!is.null(pd) && nrow(pd) > 0L) { + # Count lines with terminal tokens that are not comments + code_lines <- unique(pd$line1[pd$terminal & pd$token != "COMMENT"]) + total_lines <- total_lines + length(code_lines) + } + }, + error = function(e) { + # Skip unparseable files (lintr would skip them too) + NULL + } + ) + } - if (!is.null(pd) && nrow(pd) > 0L) { - # Count lines with terminal tokens that are not comments - code_lines <- unique(pd$line1[pd$terminal & pd$token != "COMMENT"]) - total_lines <- total_lines + length(code_lines) - } - }, error = function(e) { - # Skip unparseable files (lintr would skip them too) - NULL - }) + total_lines } - total_lines -} - # Helper: count unique (filename, line_number) pairs across all lints count_linted_lines <- function(lints) { if (length(lints) == 0L) return(0L) From 2182cd0fbe790076db17b3e28a1f4918bc9889b9 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 18 Mar 2026 13:16:42 +0100 Subject: [PATCH 16/22] refactor: split function intwo two subfuncitons --- R/data_lints.R | 134 ++++++++++++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 63 deletions(-) diff --git a/R/data_lints.R b/R/data_lints.R index 8684c1e..89488a2 100644 --- a/R/data_lints.R +++ b/R/data_lints.R @@ -2,78 +2,86 @@ # Lint metric implementation - #' Count token-bearing, non-comment lines in files that lintr processes - #' - #' Uses lintr's own parsing frontend to ensure the denominator matches - #' the scope of lint_package(). A "relevant code line" is a physical - #' line containing at least one terminal token that is not a comment. - #' - #' This approach: - #' - Matches lint_package() scope by default (configurable via pkg_dirs) - #' - Matches lint_package() file pattern by default (configurable via - #' file_pattern) - #' - Excludes blank lines and comment-only lines via parse data - #' - Correctly handles # inside strings (uses R's parser, not regex) - #' - #' @param path Path to package source directory. - #' @param pkg_dirs Character vector of subdirectories to search. Defaults to - #' the same directories as lint_package(): R/, tests/, inst/, vignettes/, - #' data-raw/, demo/, exec/. - #' @param file_pattern Regex pattern for file extensions. Defaults to the same - #' pattern as lint_dir(): .R, .Rmd, .qmd, .Rnw, .Rhtml, .Rrst, .Rtex, .Rtxt. - #' @return Integer count of code lines. - #' @noRd - count_lintable_code_lines <- function( +#' Find lintable files in a package +#' +#' @param path Package source directory. +#' @param pkg_dirs Subdirectories to search. +#' @param file_pattern Regex for file extensions. +#' @return Character vector of file paths. +#' @noRd +find_lintable_files <- function( path, pkg_dirs = c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec"), file_pattern = "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" - ) { - # Find all matching files in package directories - r_files <- character(0) - for (dir in pkg_dirs) { - dir_path <- file.path(path, dir) - if (dir.exists(dir_path)) { - files <- list.files( - dir_path, - pattern = file_pattern, - recursive = TRUE, - full.names = TRUE - ) - r_files <- c(r_files, files) - } +) { + r_files <- character(0) + for (dir in pkg_dirs) { + dir_path <- file.path(path, dir) + if (dir.exists(dir_path)) { + files <- list.files( + dir_path, + pattern = file_pattern, + recursive = TRUE, + full.names = TRUE + ) + r_files <- c(r_files, files) } + } + r_files +} - if (length(r_files) == 0L) { - return(0L) - } +#' Count token-bearing, non-comment lines in a single file +#' +#' @param file Path to an R or Rmd file. +#' @return Integer count of code lines, or 0L on parse error. +#' @noRd +count_file_code_lines <- function(file) { + tryCatch( + { + se <- lintr::get_source_expressions(file) + full_expr <- se$expressions[[length(se$expressions)]] + pd <- full_expr$full_parsed_content + + if (!is.null(pd) && nrow(pd) > 0L) { + length(unique(pd$line1[pd$terminal & pd$token != "COMMENT"])) + } else { + 0L + } + }, + error = function(e) 0L + ) +} - total_lines <- 0L - - for (file in r_files) { - tryCatch( - { - se <- lintr::get_source_expressions(file) - - # Last element contains full file parse data - full_expr <- se$expressions[[length(se$expressions)]] - pd <- full_expr$full_parsed_content - - if (!is.null(pd) && nrow(pd) > 0L) { - # Count lines with terminal tokens that are not comments - code_lines <- unique(pd$line1[pd$terminal & pd$token != "COMMENT"]) - total_lines <- total_lines + length(code_lines) - } - }, - error = function(e) { - # Skip unparseable files (lintr would skip them too) - NULL - } - ) - } +#' Count token-bearing, non-comment lines in files that lintr processes +#' +#' Uses lintr's own parsing frontend to ensure the denominator matches +#' the scope of lint_package(). A "relevant code line" is a physical +#' line containing at least one terminal token that is not a comment. +#' +#' @param path Path to package source directory. +#' @param pkg_dirs Character vector of subdirectories to search. +#' @param file_pattern Regex pattern for file extensions. +#' @return Integer count of code lines. +#' @noRd +count_lintable_code_lines <- function( + path, + pkg_dirs = c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec"), + file_pattern = "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" +) { + r_files <- find_lintable_files(path, pkg_dirs, file_pattern) - total_lines + if (length(r_files) == 0L) { + return(0L) } + total_lines <- 0L + for (file in r_files) { + total_lines <- total_lines + count_file_code_lines(file) + } + + total_lines +} + # Helper: count unique (filename, line_number) pairs across all lints count_linted_lines <- function(lints) { if (length(lints) == 0L) return(0L) From 68b3128db0e4a7664f45706f6c7e128f48ec13a7 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 18 Mar 2026 13:18:13 +0100 Subject: [PATCH 17/22] refactor: use functions instead of loops --- R/data_lints.R | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/R/data_lints.R b/R/data_lints.R index 89488a2..f6531e5 100644 --- a/R/data_lints.R +++ b/R/data_lints.R @@ -14,20 +14,19 @@ find_lintable_files <- function( pkg_dirs = c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec"), file_pattern = "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" ) { - r_files <- character(0) - for (dir in pkg_dirs) { + unlist(lapply(pkg_dirs, function(dir) { dir_path <- file.path(path, dir) if (dir.exists(dir_path)) { - files <- list.files( + list.files( dir_path, pattern = file_pattern, recursive = TRUE, full.names = TRUE ) - r_files <- c(r_files, files) + } else { + character(0) } - } - r_files + }), use.names = FALSE) } #' Count token-bearing, non-comment lines in a single file @@ -74,12 +73,7 @@ count_lintable_code_lines <- function( return(0L) } - total_lines <- 0L - for (file in r_files) { - total_lines <- total_lines + count_file_code_lines(file) - } - - total_lines + sum(viapply(r_files, count_file_code_lines)) } # Helper: count unique (filename, line_number) pairs across all lints From 6ec58b8cf4045d285b3026aa502c268ea8f698dd Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 18 Mar 2026 13:26:03 +0100 Subject: [PATCH 18/22] test: update data lints tests --- tests/testthat/test-data_lints.R | 117 +++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/tests/testthat/test-data_lints.R b/tests/testthat/test-data_lints.R index 53046e7..0703f1f 100644 --- a/tests/testthat/test-data_lints.R +++ b/tests/testthat/test-data_lints.R @@ -134,6 +134,123 @@ describe("lint_count mock implementation", { }) }) +describe("find_lintable_files helper", { + it("returns empty character vector for package with no R files", { + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "R")) + result <- val.meter:::find_lintable_files(tmp_dir) + expect_type(result, "character") + expect_length(result, 0L) + }) + + it("finds .R files in R/ directory", { + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "R")) + writeLines("x <- 1", file.path(tmp_dir, "R", "a.R")) + writeLines("y <- 2", file.path(tmp_dir, "R", "b.R")) + result <- val.meter:::find_lintable_files(tmp_dir) + expect_length(result, 2L) + expect_true(all(grepl("\\.R$", result))) + }) + + it("finds files in tests/ directory", { + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "tests", "testthat"), recursive = TRUE) + writeLines("test_that('x', {})", file.path(tmp_dir, "tests", "testthat", "test-a.R")) + result <- val.meter:::find_lintable_files(tmp_dir) + expect_length(result, 1L) + }) + + it("finds files across multiple package directories", { + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "R")) + dir.create(file.path(tmp_dir, "tests")) + dir.create(file.path(tmp_dir, "vignettes")) + writeLines("x <- 1", file.path(tmp_dir, "R", "code.R")) + writeLines("test_that('x', {})", file.path(tmp_dir, "tests", "test.R")) + writeLines("# vignette", file.path(tmp_dir, "vignettes", "intro.R")) + result <- val.meter:::find_lintable_files(tmp_dir) + expect_length(result, 3L) + }) + + it("respects custom pkg_dirs parameter", { + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "R")) + dir.create(file.path(tmp_dir, "tests")) + writeLines("x <- 1", file.path(tmp_dir, "R", "code.R")) + writeLines("test_that('x', {})", file.path(tmp_dir, "tests", "test.R")) + result <- val.meter:::find_lintable_files(tmp_dir, pkg_dirs = "R") + expect_length(result, 1L) + expect_true(grepl("/R/", result)) + }) + + it("respects custom file_pattern parameter", { + tmp_dir <- withr::local_tempdir() + dir.create(file.path(tmp_dir, "R")) + writeLines("x <- 1", file.path(tmp_dir, "R", "code.R")) + writeLines("---\ntitle: test\n---", file.path(tmp_dir, "R", "doc.Rmd")) + # Only .R files + result <- val.meter:::find_lintable_files(tmp_dir, file_pattern = "\\.R$") + expect_length(result, 1L) + expect_true(grepl("\\.R$", result)) + }) + + it("ignores non-existent directories", { + tmp_dir <- withr::local_tempdir() + # No directories created + result <- val.meter:::find_lintable_files(tmp_dir) + expect_type(result, "character") + expect_length(result, 0L) + }) +}) + +describe("count_file_code_lines helper", { + it("counts code lines in a simple R file", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + file_path <- file.path(tmp_dir, "test.R") + writeLines(c("x <- 1", "y <- 2", "z <- 3"), file_path) + result <- val.meter:::count_file_code_lines(file_path) + expect_equal(result, 3L) + }) + + it("excludes blank lines", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + file_path <- file.path(tmp_dir, "test.R") + writeLines(c("x <- 1", "", "", "y <- 2"), file_path) + result <- val.meter:::count_file_code_lines(file_path) + expect_equal(result, 2L) + }) + + it("excludes comment-only lines", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + file_path <- file.path(tmp_dir, "test.R") + writeLines(c("# comment", "x <- 1", "#' roxygen", "y <- 2"), file_path) + result <- val.meter:::count_file_code_lines(file_path) + expect_equal(result, 2L) + }) + + it("returns 0L for empty file", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + file_path <- file.path(tmp_dir, "test.R") + writeLines(character(0), file_path) + result <- val.meter:::count_file_code_lines(file_path) + expect_equal(result, 0L) + }) + + it("returns 0L for comment-only file", { + skip_if_not_installed("lintr") + tmp_dir <- withr::local_tempdir() + file_path <- file.path(tmp_dir, "test.R") + writeLines(c("# just", "# comments"), file_path) + result <- val.meter:::count_file_code_lines(file_path) + expect_equal(result, 0L) + }) +}) + describe("count_lintable_code_lines helper", { it("returns 0 for a package with no R files", { skip_if_not_installed("lintr") From 3b3c54c41a39427d2b8e51c1ecf60d5e2e580097 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 18 Mar 2026 13:28:36 +0100 Subject: [PATCH 19/22] test: apply minor style fixes --- R/data_lints.R | 12 +++++----- tests/testthat/test-data_lints.R | 41 +++++++++++++++++--------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/R/data_lints.R b/R/data_lints.R index f6531e5..1d29a1e 100644 --- a/R/data_lints.R +++ b/R/data_lints.R @@ -10,9 +10,9 @@ #' @return Character vector of file paths. #' @noRd find_lintable_files <- function( - path, - pkg_dirs = c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec"), - file_pattern = "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" + path, + pkg_dirs = c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec"), + file_pattern = "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" ) { unlist(lapply(pkg_dirs, function(dir) { dir_path <- file.path(path, dir) @@ -63,9 +63,9 @@ count_file_code_lines <- function(file) { #' @return Integer count of code lines. #' @noRd count_lintable_code_lines <- function( - path, - pkg_dirs = c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec"), - file_pattern = "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" + path, + pkg_dirs = c("R", "tests", "inst", "vignettes", "data-raw", "demo", "exec"), + file_pattern = "(?i)[.](r|rmd|qmd|rnw|rhtml|rrst|rtex|rtxt)$" ) { r_files <- find_lintable_files(path, pkg_dirs, file_pattern) diff --git a/tests/testthat/test-data_lints.R b/tests/testthat/test-data_lints.R index 0703f1f..01d9fbc 100644 --- a/tests/testthat/test-data_lints.R +++ b/tests/testthat/test-data_lints.R @@ -138,7 +138,7 @@ describe("find_lintable_files helper", { it("returns empty character vector for package with no R files", { tmp_dir <- withr::local_tempdir() dir.create(file.path(tmp_dir, "R")) - result <- val.meter:::find_lintable_files(tmp_dir) + result <- find_lintable_files(tmp_dir) expect_type(result, "character") expect_length(result, 0L) }) @@ -148,7 +148,7 @@ describe("find_lintable_files helper", { dir.create(file.path(tmp_dir, "R")) writeLines("x <- 1", file.path(tmp_dir, "R", "a.R")) writeLines("y <- 2", file.path(tmp_dir, "R", "b.R")) - result <- val.meter:::find_lintable_files(tmp_dir) + result <- find_lintable_files(tmp_dir) expect_length(result, 2L) expect_true(all(grepl("\\.R$", result))) }) @@ -156,8 +156,11 @@ describe("find_lintable_files helper", { it("finds files in tests/ directory", { tmp_dir <- withr::local_tempdir() dir.create(file.path(tmp_dir, "tests", "testthat"), recursive = TRUE) - writeLines("test_that('x', {})", file.path(tmp_dir, "tests", "testthat", "test-a.R")) - result <- val.meter:::find_lintable_files(tmp_dir) + writeLines( + "test_that('x', {})", + file.path(tmp_dir, "tests", "testthat", "test-a.R") + ) + result <- find_lintable_files(tmp_dir) expect_length(result, 1L) }) @@ -169,7 +172,7 @@ describe("find_lintable_files helper", { writeLines("x <- 1", file.path(tmp_dir, "R", "code.R")) writeLines("test_that('x', {})", file.path(tmp_dir, "tests", "test.R")) writeLines("# vignette", file.path(tmp_dir, "vignettes", "intro.R")) - result <- val.meter:::find_lintable_files(tmp_dir) + result <- find_lintable_files(tmp_dir) expect_length(result, 3L) }) @@ -179,7 +182,7 @@ describe("find_lintable_files helper", { dir.create(file.path(tmp_dir, "tests")) writeLines("x <- 1", file.path(tmp_dir, "R", "code.R")) writeLines("test_that('x', {})", file.path(tmp_dir, "tests", "test.R")) - result <- val.meter:::find_lintable_files(tmp_dir, pkg_dirs = "R") + result <- find_lintable_files(tmp_dir, pkg_dirs = "R") expect_length(result, 1L) expect_true(grepl("/R/", result)) }) @@ -190,7 +193,7 @@ describe("find_lintable_files helper", { writeLines("x <- 1", file.path(tmp_dir, "R", "code.R")) writeLines("---\ntitle: test\n---", file.path(tmp_dir, "R", "doc.Rmd")) # Only .R files - result <- val.meter:::find_lintable_files(tmp_dir, file_pattern = "\\.R$") + result <- find_lintable_files(tmp_dir, file_pattern = "\\.R$") expect_length(result, 1L) expect_true(grepl("\\.R$", result)) }) @@ -198,7 +201,7 @@ describe("find_lintable_files helper", { it("ignores non-existent directories", { tmp_dir <- withr::local_tempdir() # No directories created - result <- val.meter:::find_lintable_files(tmp_dir) + result <- find_lintable_files(tmp_dir) expect_type(result, "character") expect_length(result, 0L) }) @@ -210,7 +213,7 @@ describe("count_file_code_lines helper", { tmp_dir <- withr::local_tempdir() file_path <- file.path(tmp_dir, "test.R") writeLines(c("x <- 1", "y <- 2", "z <- 3"), file_path) - result <- val.meter:::count_file_code_lines(file_path) + result <- count_file_code_lines(file_path) expect_equal(result, 3L) }) @@ -219,7 +222,7 @@ describe("count_file_code_lines helper", { tmp_dir <- withr::local_tempdir() file_path <- file.path(tmp_dir, "test.R") writeLines(c("x <- 1", "", "", "y <- 2"), file_path) - result <- val.meter:::count_file_code_lines(file_path) + result <- count_file_code_lines(file_path) expect_equal(result, 2L) }) @@ -228,7 +231,7 @@ describe("count_file_code_lines helper", { tmp_dir <- withr::local_tempdir() file_path <- file.path(tmp_dir, "test.R") writeLines(c("# comment", "x <- 1", "#' roxygen", "y <- 2"), file_path) - result <- val.meter:::count_file_code_lines(file_path) + result <- count_file_code_lines(file_path) expect_equal(result, 2L) }) @@ -237,7 +240,7 @@ describe("count_file_code_lines helper", { tmp_dir <- withr::local_tempdir() file_path <- file.path(tmp_dir, "test.R") writeLines(character(0), file_path) - result <- val.meter:::count_file_code_lines(file_path) + result <- count_file_code_lines(file_path) expect_equal(result, 0L) }) @@ -246,7 +249,7 @@ describe("count_file_code_lines helper", { tmp_dir <- withr::local_tempdir() file_path <- file.path(tmp_dir, "test.R") writeLines(c("# just", "# comments"), file_path) - result <- val.meter:::count_file_code_lines(file_path) + result <- count_file_code_lines(file_path) expect_equal(result, 0L) }) }) @@ -256,7 +259,7 @@ describe("count_lintable_code_lines helper", { skip_if_not_installed("lintr") tmp_dir <- withr::local_tempdir() dir.create(file.path(tmp_dir, "R")) - result <- val.meter:::count_lintable_code_lines(tmp_dir) + result <- count_lintable_code_lines(tmp_dir) expect_equal(result, 0L) }) @@ -268,7 +271,7 @@ describe("count_lintable_code_lines helper", { writeLines(c("x <- 1", "", "y <- 2"), file.path(tmp_dir, "R", "a.R")) # Line 1: code writeLines(c("z <- 3"), file.path(tmp_dir, "R", "b.R")) - result <- val.meter:::count_lintable_code_lines(tmp_dir) + result <- count_lintable_code_lines(tmp_dir) expect_equal(result, 3L) }) @@ -281,7 +284,7 @@ describe("count_lintable_code_lines helper", { c("# This is a comment", "x <- 1", "", "#' roxygen", "y <- 2"), file.path(tmp_dir, "R", "a.R") ) - result <- val.meter:::count_lintable_code_lines(tmp_dir) + result <- count_lintable_code_lines(tmp_dir) expect_equal(result, 2L) }) @@ -293,7 +296,7 @@ describe("count_lintable_code_lines helper", { writeLines("x <- 1", file.path(tmp_dir, "R", "a.R")) writeLines("test_that('works', { expect_true(TRUE) })", file.path(tmp_dir, "tests", "testthat", "test-a.R")) - result <- val.meter:::count_lintable_code_lines(tmp_dir) + result <- count_lintable_code_lines(tmp_dir) # 1 from R/, 1 from tests/ expect_equal(result, 2L) }) @@ -301,7 +304,7 @@ describe("count_lintable_code_lines helper", { describe("count_linted_lines helper", { it("returns 0 for empty lints", { - result <- val.meter:::count_linted_lines(create_mock_lints(0)) + result <- count_linted_lines(create_mock_lints(0)) expect_equal(result, 0L) }) @@ -326,7 +329,7 @@ describe("count_linted_lines helper", { ), class = c("lints", "list") ) - result <- val.meter:::count_linted_lines(lints_same_line) + result <- count_linted_lines(lints_same_line) expect_equal(result, 2L) }) }) From 1d9efc5b25fadd491043ee47490f4c643adbdc5a Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 18 Mar 2026 14:01:36 +0100 Subject: [PATCH 20/22] docs: add explanation of linter loading --- R/data_lints.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/data_lints.R b/R/data_lints.R index 1d29a1e..6b40f63 100644 --- a/R/data_lints.R +++ b/R/data_lints.R @@ -107,6 +107,8 @@ impl_data( for_resource = source_code_resource, function(pkg, resource, field, ..., quiet = opt("quiet")) { linter_names <- opt("lint_linters") + # We are loading linter objects by name here to avoid hard dependencies + # on lintr in the rest of the code. linters <- setNames( lapply(linter_names, function(nm) utils::getFromNamespace(nm, "lintr")()), linter_names From 857ac0ded14a1b5e7315b8d91afc01005371df15 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 18 Mar 2026 14:03:32 +0100 Subject: [PATCH 21/22] docs: recompile documentation --- man/metrics.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/metrics.Rd b/man/metrics.Rd index 56f6abb..1fbd6c0 100644 --- a/man/metrics.Rd +++ b/man/metrics.Rd @@ -42,7 +42,7 @@ For access to \emph{all} the internally calculated data, pass \code{all = TRUE}. } \subsection{Lint-Free Fraction}{ -\code{} Fraction of non-empty R source lines that are free of lints, as identified by \pkg{lintr}. A value of 1 means no linted lines; lower values indicate more pervasive lint issues. The set of linters applied is controlled by the \code{lint_linters} option. Returns \code{1} when the package has no R source lines. +\code{} Fraction of R code lines that are free of lints. Code lines are defined as lines containing executable R tokens (excluding comments and blank lines) across the standard package directories (\code{R/}, \code{tests/}, \code{inst/}, \code{vignettes/}, \code{data-raw/}, \code{demo/}, \code{exec/}). A value of 1 means no linted lines; lower values indicate more pervasive lint issues. The set of linters applied is controlled by the \code{lint_linters} option. From 18e36ddccb062b03ed361caad200dd4529600e12 Mon Sep 17 00:00:00 2001 From: Dominik Rafacz Date: Wed, 18 Mar 2026 14:05:34 +0100 Subject: [PATCH 22/22] style: fix linting issue (ironic) --- R/data_lints.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/data_lints.R b/R/data_lints.R index 6b40f63..739d966 100644 --- a/R/data_lints.R +++ b/R/data_lints.R @@ -107,7 +107,7 @@ impl_data( for_resource = source_code_resource, function(pkg, resource, field, ..., quiet = opt("quiet")) { linter_names <- opt("lint_linters") - # We are loading linter objects by name here to avoid hard dependencies + # We are loading linter objects by name here to avoid hard dependencies # on lintr in the rest of the code. linters <- setNames( lapply(linter_names, function(nm) utils::getFromNamespace(nm, "lintr")()),