diff --git a/DESCRIPTION b/DESCRIPTION index 63461cd..c670c45 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: box.linters Title: Linters for 'box' Modules -Version: 0.9.1.9004 +Version: 0.9.1.9005 Authors@R: c( person("Ricardo Rodrigo", "Basa", role = c("aut", "cre"), email = "opensource+rodrigo@appsilon.com"), diff --git a/NAMESPACE b/NAMESPACE index 18c95de..fb11f87 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -13,6 +13,7 @@ export(box_unused_att_pkg_fun_linter) export(box_unused_attached_mod_linter) export(box_unused_attached_pkg_linter) export(box_usage_linter) +export(namespaced_function_calls) export(r6_usage_linter) export(rhino_default_linters) export(style_box_use_dir) diff --git a/NEWS.md b/NEWS.md index 4d83926..743563a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # box.linters (development version) +* Add checks for `package::function()` calls. Allow `box::*()` by default. * [Bug fix] Allow relative box module paths (#110) * Less verbose `box_alphabetical_calls_linter()`. Reports only the first out-of-place function. * Added styling functions for `box::use()` calls. diff --git a/R/namespaced_function_calls.R b/R/namespaced_function_calls.R new file mode 100644 index 0000000..bcd8d3d --- /dev/null +++ b/R/namespaced_function_calls.R @@ -0,0 +1,89 @@ +#' Check that `namespace::function()` calls except for `box::*()` are not made. +#' +#' @param allow Character vector of `namespace` or `namespace::function` to allow in the source +#' code. Take not that the `()` are not included. The `box` namespace will always be allowed +#' +#' @examples +#' # will produce lints +#' code <- "box::use(package) +#' tidyr::pivot_longer()" +#' +#' lintr::lint(text = code, linters = namespaced_function_calls()) +#' +#' ## allow `tidyr::pivot_longer()` +#' code <- "box::use(package) +#' tidyr::pivot_longer() +#' tidyr::pivot_wider()" +#' +#' lintr::lint(text = code, linters = namespaced_function_calls(allow = c("tidyr::pivot_longer"))) +#' +#' # okay +#' code <- "box::use(package)" +#' +#' lintr::lint(text = code, linters = namespaced_function_calls()) +#' +#' ## allow all `tidyr` +#' code <- "box::use(package) +#' tidyr::pivot_longer() +#' tidyr::pivot_wider()" +#' +#' lintr::lint(text = code, linters = namespaced_function_calls(allow = c("tidyr"))) +#' +#' ## allow `tidyr::pivot_longer()` +#' code <- "box::use(package) +#' tidyr::pivot_longer()" +#' +#' lintr::lint(text = code, linters = namespaced_function_calls(allow = c("tidyr::pivot_longer"))) +#' +#' @export +namespaced_function_calls <- function(allow = NULL) { + always_allow <- c("box") + + allow <- c(always_allow, allow) + + ts_query <- " +function: (namespace_operator + lhs: (identifier) @pkg_namespace + operator: \"::\" + rhs: (identifier) +) @full_namespace" + + lint_message <- "Explicit `package::function()` calls are not advisible when using `box` modules." + + lintr::Linter(function(source_expression) { + if (!lintr::is_lint_level(source_expression, "file")) { + return(list()) + } + source_text <- paste(source_expression$file_lines, collapse = "\n") + + tree_root <- ts_root(source_text) + + tree_namespaced <- ts_find_all(tree_root, ts_query) + + lapply(tree_namespaced[[1]], function(tree_node) { + full_idx <- match("full_namespace", tree_node$name) + full_text <- treesitter::node_text(tree_node$node[[full_idx]]) + pkg_idx <- match("pkg_namespace", tree_node$name) + pkg_text <- treesitter::node_text(tree_node$node[[pkg_idx]]) + + namespaced_calls <- c(full_text, pkg_text) + + if (all(!namespaced_calls %in% allow)) { + start_where <- treesitter::node_start_point(tree_node$node[[full_idx]]) + line_number <- treesitter::point_row(start_where) + 1 + start_col_number <- treesitter::point_column(start_where) + 1 + end_where <- treesitter::node_end_point(tree_node$node[[full_idx]]) + end_col_number <- treesitter::point_column(end_where) + lintr::Lint( + source_expression$filename, + line_number = line_number, + column_number = start_col_number, + type = "warning", + message = lint_message, + line = source_expression$file_lines[line_number], + ranges = list(c(start_col_number, end_col_number)) + ) + } + }) + }) +} diff --git a/R/zzz.R b/R/zzz.R index 664c712..dc75a6a 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -25,6 +25,7 @@ rhino_default_linters <- lintr::modify_defaults( box_unused_att_pkg_linter = box_unused_attached_pkg_linter(), box_unused_attached_pkg_fun_linter = box_unused_att_pkg_fun_linter(), box_usage_linter = box_usage_linter(), + namespaced_function_calls = namespaced_function_calls(), r6_usage_linter = r6_usage_linter(), unused_declared_object_linter = unused_declared_object_linter(), object_usage_linter = NULL # Does not work with `box::use()` @@ -49,6 +50,7 @@ box_default_linters <- lintr::modify_defaults( box_unused_att_pkg_linter = box_unused_attached_pkg_linter(), box_unused_attached_pkg_fun_linter = box_unused_att_pkg_fun_linter(), box_usage_linter = box_usage_linter(), + namespaced_function_calls = namespaced_function_calls(), r6_usage_linter = r6_usage_linter(), unused_declared_object_linter = unused_declared_object_linter(), object_usage_linter = NULL # Does not work with `box::use()` diff --git a/man/namespaced_function_calls.Rd b/man/namespaced_function_calls.Rd new file mode 100644 index 0000000..0ffb8d3 --- /dev/null +++ b/man/namespaced_function_calls.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/namespaced_function_calls.R +\name{namespaced_function_calls} +\alias{namespaced_function_calls} +\title{Check that \verb{namespace::function()} calls except for \verb{box::*()} are not made.} +\usage{ +namespaced_function_calls(allow = NULL) +} +\arguments{ +\item{allow}{Character vector of \code{namespace} or \verb{namespace::function} to allow in the source +code. Take not that the \verb{()} are not included. The \code{box} namespace will always be allowed} +} +\description{ +Check that \verb{namespace::function()} calls except for \verb{box::*()} are not made. +} +\examples{ +# will produce lints +code <- "box::use(package) +tidyr::pivot_longer()" + +lintr::lint(text = code, linters = namespaced_function_calls()) + +## allow `tidyr::pivot_longer()` +code <- "box::use(package) +tidyr::pivot_longer() +tidyr::pivot_wider()" + +lintr::lint(text = code, linters = namespaced_function_calls(allow = c("tidyr::pivot_longer"))) + +# okay +code <- "box::use(package)" + +lintr::lint(text = code, linters = namespaced_function_calls()) + +## allow all `tidyr` +code <- "box::use(package) +tidyr::pivot_longer() +tidyr::pivot_wider()" + +lintr::lint(text = code, linters = namespaced_function_calls(allow = c("tidyr"))) + +## allow `tidyr::pivot_longer()` +code <- "box::use(package) +tidyr::pivot_longer()" + +lintr::lint(text = code, linters = namespaced_function_calls(allow = c("tidyr::pivot_longer"))) + +} diff --git a/tests/testthat/test-namespaced_function_calls.R b/tests/testthat/test-namespaced_function_calls.R new file mode 100644 index 0000000..9493782 --- /dev/null +++ b/tests/testthat/test-namespaced_function_calls.R @@ -0,0 +1,43 @@ +test_that("namespaced_function_calls() skips allowed box::*() calls", { + linter <- namespaced_function_calls() + + code <- "box::use(package) +box::export() +box::file() +box::name()" + + lintr::expect_lint(code, NULL, linter) +}) + +test_that("namespaced_function_calls() blocks non-box namespace calls", { + linter <- namespaced_function_calls() + lint_message <- rex::rex( + "Explicit `package::function()` calls are not advisible when using `box` modules." + ) + + code <- "box::use(package) + tidyr::pivot_longer() + " + + lintr::expect_lint(code, list(message = lint_message), linter) +}) + +test_that("namespaced_function_calls() skips excluded namespace calls", { + linter <- namespaced_function_calls(allow = c("tidyr")) + + code <- "box::use(package) + tidyr::pivot_longer() + " + + lintr::expect_lint(code, NULL, linter) +}) + +test_that("namespaced_function_calls() skips excluded namespace::function calls", { + linter <- namespaced_function_calls(allow = c("tidyr::pivot_longer")) + + code <- "box::use(package) + tidyr::pivot_longer() + " + + lintr::expect_lint(code, NULL, linter) +})