From 9dc5465dd9f55f1540027c439250b2818aaf7860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Thu, 18 Jul 2024 15:55:30 +0200 Subject: [PATCH 01/16] feat: create a new roxygen tag for shiny modules --- DESCRIPTION | 4 ++-- NAMESPACE | 9 +++++++++ R/module_roclet.R | 36 ++++++++++++++++++++++++++++++++++++ man/shinyModule_roclet.Rd | 11 +++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 R/module_roclet.R create mode 100644 man/shinyModule_roclet.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 4fa54436..91265b3b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -31,6 +31,7 @@ Imports: here, htmltools, rlang (>= 1.0.0), + roxygen2, shiny (>= 1.5.0), utils, yaml @@ -55,7 +56,6 @@ Suggests: remotes, renv, rmarkdown, - roxygen2, rsconnect, rstudioapi, sass, @@ -71,4 +71,4 @@ Config/testthat/edition: 3 Encoding: UTF-8 Language: en-US Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2 diff --git a/NAMESPACE b/NAMESPACE index 37bd5092..7dfc78c7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,8 @@ # Generated by roxygen2: do not edit by hand +S3method(format,rd_section_shinyModule) +S3method(roxy_tag_parse,roxy_tag_shinyModule) +S3method(roxy_tag_rd,roxy_tag_shinyModule) export(activate_js) export(add_css_file) export(add_dockerfile) @@ -76,6 +79,7 @@ export(set_golem_name) export(set_golem_options) export(set_golem_version) export(set_golem_wd) +export(shinyModule_roclet) export(use_external_css_file) export(use_external_file) export(use_external_html_template) @@ -102,6 +106,11 @@ importFrom(attempt,stop_if_not) importFrom(attempt,without_warning) importFrom(config,get) importFrom(htmltools,htmlDependency) +importFrom(roxygen2,rd_section) +importFrom(roxygen2,roclet) +importFrom(roxygen2,roxy_tag_parse) +importFrom(roxygen2,roxy_tag_rd) +importFrom(roxygen2,tag_markdown) importFrom(shiny,addResourcePath) importFrom(shiny,getShinyOption) importFrom(shiny,htmlTemplate) diff --git a/R/module_roclet.R b/R/module_roclet.R new file mode 100644 index 00000000..aaef7671 --- /dev/null +++ b/R/module_roclet.R @@ -0,0 +1,36 @@ +#' @importFrom roxygen2 roxy_tag_parse +#' @importFrom roxygen2 roxy_tag_rd +NULL + +#' @importFrom roxygen2 tag_markdown +#' @export +roxy_tag_parse.roxy_tag_shinyModule <- function(x) { + tag_markdown( + x = x + ) +} + +#' @importFrom roxygen2 rd_section +#' @export +roxy_tag_rd.roxy_tag_shinyModule <- function(x, base_path, env) { + rd_section( + type = "shinyModule", + value = x$val + ) +} + +#' @export +format.rd_section_shinyModule <- function(x, ...) { + paste0( + "\\section{Shiny module}{\n", + x$value, + "\n}" + ) +} + +#' shinyModule tag +#' @importFrom roxygen2 roclet +#' @export +shinyModule_roclet <- function() { + roclet("shinyModule") +} diff --git a/man/shinyModule_roclet.Rd b/man/shinyModule_roclet.Rd new file mode 100644 index 00000000..1c67a589 --- /dev/null +++ b/man/shinyModule_roclet.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/module_roclet.R +\name{shinyModule_roclet} +\alias{shinyModule_roclet} +\title{shinyModule tag} +\usage{ +shinyModule_roclet() +} +\description{ +shinyModule tag +} From 2043559318b27bd1c98e97cfcce34f3ff44ee825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Thu, 18 Jul 2024 17:02:39 +0200 Subject: [PATCH 02/16] feat: add shiny module tag in template --- R/modules_fn.R | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/R/modules_fn.R b/R/modules_fn.R index 9cba26c8..09ce5a62 100644 --- a/R/modules_fn.R +++ b/R/modules_fn.R @@ -26,20 +26,19 @@ #' #' @return The path to the file, invisibly. add_module <- function( - name, - pkg = get_golem_wd(), - open = TRUE, - dir_create = TRUE, - fct = NULL, - utils = NULL, - r6 = NULL, - js = NULL, - js_handler = NULL, - export = FALSE, - module_template = golem::module_template, - with_test = FALSE, - ... - ) { + name, + pkg = get_golem_wd(), + open = TRUE, + dir_create = TRUE, + fct = NULL, + utils = NULL, + r6 = NULL, + js = NULL, + js_handler = NULL, + export = FALSE, + module_template = golem::module_template, + with_test = FALSE, + ...) { # Let's start with the checks for the validity of the name check_name_length_is_one(name) check_name_syntax(name) @@ -187,13 +186,12 @@ add_module <- function( #' @export #' @seealso [add_module()] module_template <- function( - name, - path, - export, - ph_ui = " ", - ph_server = " ", - ... - ) { + name, + path, + export, + ph_ui = " ", + ph_server = " ", + ...) { write_there <- function(...) { write(..., file = path, append = TRUE) } @@ -204,6 +202,7 @@ module_template <- function( write_there("#'") write_there("#' @param id,input,output,session Internal parameters for {shiny}.") write_there("#'") + write_there("#' @shinyModule A Golem module.") if (export) { write_there(sprintf("#' @rdname mod_%s", name)) write_there("#' @export ") @@ -274,10 +273,9 @@ module_template <- function( #' @return Used for side effect. Returns the path invisibly. #' @export use_module_test <- function( - name, - pkg = get_golem_wd(), - open = TRUE - ) { + name, + pkg = get_golem_wd(), + open = TRUE) { # Remove the extension if any name <- file_path_sans_ext(name) # Remove the "mod_" if any From 01fea1cdb29cffd863916c090a8fabcff2f36d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Thu, 18 Jul 2024 17:02:54 +0200 Subject: [PATCH 03/16] test: test module template output --- tests/testthat/test-module_template.R | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/testthat/test-module_template.R diff --git a/tests/testthat/test-module_template.R b/tests/testthat/test-module_template.R new file mode 100644 index 00000000..4ae113c3 --- /dev/null +++ b/tests/testthat/test-module_template.R @@ -0,0 +1,50 @@ +test_that("Check module_template output", { + tmp <- tempdir() + + unlink(file.path(tmp, "test.R")) + + module_template( + name = "test", + path = file.path(tmp, "test.R"), + export = TRUE + ) + + file_content <- readLines(file.path(tmp, "test.R")) + + tags <- c( + "@export", + "@shinyModule", + "@rdname mod_test" + ) + + expect_true( + sapply( + tags, + \(x) any(grepl(x, file_content)) + ) |> + all() + ) + + unlink(file.path(tmp, "test.R")) + + module_template( + name = "test", + path = file.path(tmp, "test.R"), + export = FALSE + ) + + file_content <- readLines(file.path(tmp, "test.R")) + + tags <- c( + "@noRd", + "@shinyModule" + ) + + expect_true( + sapply( + tags, + \(x) any(grepl(x, file_content)) + ) |> + all() + ) +}) From b3d62d26e7e37e7b5e90ca9376b1e298bc4bc8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Fri, 19 Jul 2024 12:02:34 +0200 Subject: [PATCH 04/16] feat: add a break line --- R/modules_fn.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/modules_fn.R b/R/modules_fn.R index 09ce5a62..cb787b89 100644 --- a/R/modules_fn.R +++ b/R/modules_fn.R @@ -203,6 +203,7 @@ module_template <- function( write_there("#' @param id,input,output,session Internal parameters for {shiny}.") write_there("#'") write_there("#' @shinyModule A Golem module.") + write_there("#'") if (export) { write_there(sprintf("#' @rdname mod_%s", name)) write_there("#' @export ") From 0fbe4d4364de5b8c9914a525728c198ada2ef83c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Fri, 19 Jul 2024 17:22:53 +0200 Subject: [PATCH 05/16] feat: detect missing NS --- NAMESPACE | 2 + R/find_missing_ns.R | 171 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 R/find_missing_ns.R diff --git a/NAMESPACE b/NAMESPACE index 7dfc78c7..0467375f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -37,6 +37,7 @@ export(browser_button) export(browser_dev) export(bundle_resources) export(cat_dev) +export(check_namespace_sanity) export(create_golem) export(css_template) export(detach_all_attached) @@ -121,6 +122,7 @@ importFrom(tools,file_ext) importFrom(utils,capture.output) importFrom(utils,file.edit) importFrom(utils,getFromNamespace) +importFrom(utils,getParseData) importFrom(utils,menu) importFrom(utils,modifyList) importFrom(utils,person) diff --git a/R/find_missing_ns.R b/R/find_missing_ns.R new file mode 100644 index 00000000..c2f62b59 --- /dev/null +++ b/R/find_missing_ns.R @@ -0,0 +1,171 @@ +#' @noRd +stop_quietly <- function() { + opt <- options(show.error.messages = FALSE) + on.exit(options(opt)) + stop() +} + +#' @noRd +is_shiny_input_output_funmodule <- function( + text, + extend_input_output_funmodule = NA_character_ +) { + stopifnot(is.character(extend_input_output_funmodule)) + + input_output_knew <- c("Input|Output|actionButton|radioButtons") + ui_funmodule_pattern <- c("mod_\\w+_ui") + + patterns <- paste( + input_output_knew, + ui_funmodule_pattern, + sep = "|" + ) + + if ( + !is.null(extend_input_output_funmodule) || + !is.na(extend_input_output_funmodule) || + extend_input_output_funmodule == "" + ) { + patterns <- paste( + patterns, + extend_input_output_funmodule, + sep = "|" + ) + } + + grepl( + pattern = patterns, + x = text + ) +} + +#' @noRd +#' @importFrom utils getParseData +check_namespace_in_file <- function( + path, + extend_input_output_funmodule = NA_character_ +) { + getParseData( + parse( + file = path, + keep.source = TRUE + ) + ) |> + dplyr::mutate( + path = path + ) |> + dplyr::filter( + token == "SYMBOL_FUNCTION_CALL" + ) |> + dplyr::mutate( + is_input_output_funmodule = is_shiny_input_output_funmodule( + text = text, + extend_input_output_funmodule = extend_input_output_funmodule + ) + ) |> + dplyr::mutate( + is_followed_by = dplyr::lead(text), + is_followed_by_ns = is_followed_by == "ns" + ) |> + dplyr::filter( + is_input_output_funmodule + ) +} + +#' @export +check_namespace_sanity <- function( + pkg = golem::get_golem_wd(), + extend_input_output_funmodule = NA_character_, + disable = FALSE +) { + if (disable) { + return(invisible(FALSE)) + } + + base_path <- normalizePath( + path = pkg, + mustWork = TRUE + ) + + if (!requireNamespace("desc", quietly = TRUE)) { + check_desc_installed() + } + + encoding <- desc::desc_get("Encoding", file = base_path)[[1]] + + if (!identical(encoding, "UTF-8")) { + warning("roxygen2 requires Encoding: UTF-8", call. = FALSE) + } + + blocks <- roxygen2::parse_package( + path = ".", + env = NULL + ) + + shinymodule_blocks <- blocks |> + purrr::map( + .f = \(x) { + return <- roxygen2::block_get_tag(x, tag = "shinyModule") + if (is.null(return)) { + NULL + } else { + return + } + } + ) |> + purrr::compact() + + if (length(shinymodule_blocks) == 0) { + cli::cli_alert_info("ok") + return(invisible(FALSE)) + } + + shinymodule_links <- shinymodule_blocks |> + purrr::map_chr( + .f = ~ .x[["file"]] + ) |> + unique() + + data <- shinymodule_links |> + purrr::map_df( + .f = ~ check_namespace_in_file( + path = .x, + extend_input_output_funmodule = extend_input_output_funmodule + ) + ) |> + dplyr::filter( + !is_followed_by_ns + ) |> + dplyr::select( + path, + text, + line1, + col1, + is_followed_by, + is_followed_by_ns + ) |> + dplyr::mutate( + message = sprintf("... see line %d in {.file %s:%d:%d}.", line1, path, line1, col1) + ) + + missing_ns_detected <- nrow(data) + + cli::cli_text( + "It seems that ", + cli::bg_br_yellow( + "{missing_ns_detected} namespace{?s} (NS) {?is/are} missing..." + ) + ) + + cli::cli_alert_info("Fix {?this/these} {missing_ns_detected} missing namespace{?s} before to continue...") + + purrr::walk(data$message, cli::cli_alert_danger) + + launch_app <- yesno::yesno("Is it fixed? Do you want to launch the app?") + + if (isFALSE(launch_app)) { + stop_quietly() + } + + return(TRUE) +} From 9ec9fe8e48f8c5f23bfe765e1504d9c5d23ff26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Fri, 19 Jul 2024 17:34:00 +0200 Subject: [PATCH 06/16] chore: add function documentation --- R/find_missing_ns.R | 9 +++++++++ man/check_namespace_sanity.Rd | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 man/check_namespace_sanity.Rd diff --git a/R/find_missing_ns.R b/R/find_missing_ns.R index c2f62b59..b584c064 100644 --- a/R/find_missing_ns.R +++ b/R/find_missing_ns.R @@ -72,6 +72,15 @@ check_namespace_in_file <- function( ) } +#' check namespace sanity +#' Will check if the namespace (NS) are correctly set in the shiny modules +#' +#' @param pkg The package path +#' @param extend_input_output_funmodule Extend the input, output or function module to check +#' @param disable Disable the check +#' +#' @return Logical. TRUE if the namespace are correctly set, FALSE otherwise +#' #' @export check_namespace_sanity <- function( pkg = golem::get_golem_wd(), diff --git a/man/check_namespace_sanity.Rd b/man/check_namespace_sanity.Rd new file mode 100644 index 00000000..67831335 --- /dev/null +++ b/man/check_namespace_sanity.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/find_missing_ns.R +\name{check_namespace_sanity} +\alias{check_namespace_sanity} +\title{check namespace sanity +Will check if the namespace (NS) are correctly set in the shiny modules} +\usage{ +check_namespace_sanity( + pkg = golem::get_golem_wd(), + extend_input_output_funmodule = NA_character_, + disable = FALSE +) +} +\arguments{ +\item{pkg}{The package path} + +\item{extend_input_output_funmodule}{Extend the input, output or function module to check} + +\item{disable}{Disable the check} +} +\value{ +Logical. TRUE if the namespace are correctly set, FALSE otherwise +} +\description{ +check namespace sanity +Will check if the namespace (NS) are correctly set in the shiny modules +} From 010e56998647120a9f5e4c0bbcfbca9944380e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Fri, 19 Jul 2024 17:36:03 +0200 Subject: [PATCH 07/16] chore: check in the run_dev template --- inst/shinyexample/dev/run_dev.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inst/shinyexample/dev/run_dev.R b/inst/shinyexample/dev/run_dev.R index 08030f44..43a74d9d 100644 --- a/inst/shinyexample/dev/run_dev.R +++ b/inst/shinyexample/dev/run_dev.R @@ -8,6 +8,9 @@ options(shiny.port = httpuv::randomPort()) golem::detach_all_attached() # rm(list=ls(all.names = TRUE)) +# Check for missing namespaces +check_namespace_sanity(disable = FALSE) + # Document and reload your package golem::document_and_reload() From 3c3d160dce28c739deb5427c062b8e7f33f8be79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Fri, 19 Jul 2024 17:49:05 +0200 Subject: [PATCH 08/16] feat: stop quickly and safely if NS checked ok --- R/find_missing_ns.R | 7 ++++++- inst/shinyexample/dev/run_dev.R | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/R/find_missing_ns.R b/R/find_missing_ns.R index b584c064..935070fa 100644 --- a/R/find_missing_ns.R +++ b/R/find_missing_ns.R @@ -125,7 +125,7 @@ check_namespace_sanity <- function( purrr::compact() if (length(shinymodule_blocks) == 0) { - cli::cli_alert_info("ok") + cli::cli_alert_info("No shiny module found") return(invisible(FALSE)) } @@ -159,6 +159,11 @@ check_namespace_sanity <- function( missing_ns_detected <- nrow(data) + if (missing_ns_detected == 0) { + cli::cli_alert_success("NS check passed") + return(invisible(TRUE)) + } + cli::cli_text( "It seems that ", cli::bg_br_yellow( diff --git a/inst/shinyexample/dev/run_dev.R b/inst/shinyexample/dev/run_dev.R index 43a74d9d..cb3afb91 100644 --- a/inst/shinyexample/dev/run_dev.R +++ b/inst/shinyexample/dev/run_dev.R @@ -9,7 +9,7 @@ golem::detach_all_attached() # rm(list=ls(all.names = TRUE)) # Check for missing namespaces -check_namespace_sanity(disable = FALSE) +golem::check_namespace_sanity(disable = FALSE) # Document and reload your package golem::document_and_reload() From 844da7da67aaded29f313586780b24f007edb6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Mon, 22 Jul 2024 17:32:48 +0200 Subject: [PATCH 09/16] chore: add packages deps --- NAMESPACE | 2 ++ R/find_missing_ns.R | 13 +++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 0467375f..58a443e0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -107,6 +107,8 @@ importFrom(attempt,stop_if_not) importFrom(attempt,without_warning) importFrom(config,get) importFrom(htmltools,htmlDependency) +importFrom(roxygen2,block_get_tag) +importFrom(roxygen2,parse_package) importFrom(roxygen2,rd_section) importFrom(roxygen2,roclet) importFrom(roxygen2,roxy_tag_parse) diff --git a/R/find_missing_ns.R b/R/find_missing_ns.R index 935070fa..0a6cfb77 100644 --- a/R/find_missing_ns.R +++ b/R/find_missing_ns.R @@ -79,6 +79,8 @@ check_namespace_in_file <- function( #' @param extend_input_output_funmodule Extend the input, output or function module to check #' @param disable Disable the check #' +#' @importFrom roxygen2 parse_package block_get_tag +#' #' @return Logical. TRUE if the namespace are correctly set, FALSE otherwise #' #' @export @@ -91,15 +93,14 @@ check_namespace_sanity <- function( return(invisible(FALSE)) } + check_desc_installed() + check_cli_installed() + base_path <- normalizePath( path = pkg, mustWork = TRUE ) - if (!requireNamespace("desc", quietly = TRUE)) { - check_desc_installed() - } - encoding <- desc::desc_get("Encoding", file = base_path)[[1]] if (!identical(encoding, "UTF-8")) { @@ -175,11 +176,11 @@ check_namespace_sanity <- function( purrr::walk(data$message, cli::cli_alert_danger) - launch_app <- yesno::yesno("Is it fixed? Do you want to launch the app?") + launch_app <- yesno("Is it fixed? Do you want to launch the app?") if (isFALSE(launch_app)) { stop_quietly() } - return(TRUE) + return(invisible(TRUE)) } From 2c42fdbb315a72879e4baa47870c2cb095e429a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Mon, 22 Jul 2024 17:33:04 +0200 Subject: [PATCH 10/16] test: check missing ns --- tests/testthat/test-find_missing_ns.R | 246 ++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 tests/testthat/test-find_missing_ns.R diff --git a/tests/testthat/test-find_missing_ns.R b/tests/testthat/test-find_missing_ns.R new file mode 100644 index 00000000..6d8890a8 --- /dev/null +++ b/tests/testthat/test-find_missing_ns.R @@ -0,0 +1,246 @@ +test_that("check is_shiny_input_output_funmodule works", { + expect_true( + all( + is_shiny_input_output_funmodule( + text = c( + "textInput", + "textOutput", + "actionButton", + "mod_test_module_ui" + ) + ) + ) + ) + + expect_true( + all( + is_shiny_input_output_funmodule( + text = c( + "textInput", + "textOutput", + "actionButton", + "mod_test_module_ui", + "sk_select_input" + ), + extend_input_output_funmodule = "sk_select_input" + ) + ) + ) + + expect_false( + all( + is_shiny_input_output_funmodule( + text = c( + "textInput", + "textOutput", + "mod_test_module" + ) + ) + ) + ) +}) + + +test_that("Check check_namespace_in_file", { + path <- tempfile(fileext = ".R") + + writeLines( + c( + "mod_test_module_ui <- function(id) {", + " ns <- NS(id)", + " tagList(", + " selectInput(", + " inputId = 'selectinput',", # Missing NS + " label = 'Select input',", + " choices = c(LETTERS[1:10])", + " ),", + " actionButton(", + " inputId = 'actionbutton',", # Missing NS + " label = 'Action button'", + " )", + " )", + "}", + "", + "mod_test_module_server <- function(id) {", + " observeEvent(input$actionbutton, {", + " message(input$actionbutton)", + " message(input$selectinput)", + " })", + "}" + ), + con = path + ) + + expect_equal( + check_namespace_in_file( + path = path, + ), + structure( + list( + line1 = c(4L, 9L), + col1 = c(5L, 5L), + line2 = c(4L, 9L), + col2 = 15:16, + id = c(32L, 89L), + parent = c(34L, 91L), + token = c( + "SYMBOL_FUNCTION_CALL", + "SYMBOL_FUNCTION_CALL" + ), + terminal = c(TRUE, TRUE), + text = c("selectInput", "actionButton"), + path = rep(path, 2), + is_input_output_funmodule = c(TRUE, TRUE), # 2 TRUE + is_followed_by = c("c", "observeEvent"), + is_followed_by_ns = c(FALSE, FALSE) # 2 FALSE + ), + row.names = c(NA, -2L), + class = "data.frame" + ), + ignore_attr = TRUE + ) + + writeLines( + c( + "mod_test_module_ui <- function(id) {", + " ns <- NS(id)", + " tagList(", + " selectInput(", + " inputId = ns('selectinput'),", # NS present + " label = 'Select input',", + " choices = c(LETTERS[1:10])", + " ),", + " actionButton(", + " inputId = ns('actionbutton'),", # NS present + " label = 'Action button'", + " )", + " )", + "}", + "", + "mod_test_module_server <- function(id) {", + " observeEvent(input$actionbutton, {", + " message(input$actionbutton)", + " message(input$selectinput)", + " })", + "}" + ), + con = path + ) + + expect_equal( + check_namespace_in_file( + path = path, + ), + structure( + list( + line1 = c(4L, 9L), + col1 = c(5L, 5L), + line2 = c(4L, 9L), + col2 = 15:16, + id = c(32L, 97L), + parent = c(34L, 99L), + token = c( + "SYMBOL_FUNCTION_CALL", + "SYMBOL_FUNCTION_CALL" + ), + terminal = c(TRUE, TRUE), + text = c( + "selectInput", + "actionButton" + ), + path = rep(path, 2), + is_input_output_funmodule = c(TRUE, TRUE), + is_followed_by = c("ns", "ns"), + is_followed_by_ns = c(TRUE, TRUE) + ), + row.names = c(NA, -2L), + class = "data.frame" + ), + ignore_attr = TRUE + ) + + writeLines( + c( + "mod_test_module_ui <- function(id) {", + " ns <- NS(id)", + " tagList(", + " sk_select_input(", + " inputId = 'selectinput',", # missing NS with custom fun + " label = 'Select input',", + " choices = c(LETTERS[1:10])", + " ),", + " mod_test_2_module_ui(", + " id = ns('actionbutton'),", # NS present + " )", + " )", + "}", + "", + "mod_test_module_server <- function(id) {", + " observeEvent(input$actionbutton, {", + " message(input$actionbutton)", + " message(input$selectinput)", + " })", + "}" + ), + con = path + ) + + expect_equal( + check_namespace_in_file( + path = path + ), + structure( + list( + line1 = 9L, + col1 = 5L, + line2 = 9L, + col2 = 24L, + id = 89L, + parent = 91L, + token = "SYMBOL_FUNCTION_CALL", + terminal = TRUE, + text = "mod_test_2_module_ui", + path = path, + is_input_output_funmodule = TRUE, + is_followed_by = "ns", + is_followed_by_ns = TRUE + ), + row.names = c(NA, -1L), + class = "data.frame" + ), + ignore_attr = TRUE + ) + + expect_equal( + check_namespace_in_file( + path = path, + extend_input_output_funmodule = "sk_select_input" + ), + structure( + list( + line1 = c(4L, 9L), + col1 = c(5L, 5L), + line2 = c(4L, 9L), + col2 = c(19L, 24L), + id = c(32L, 89L), + parent = c(34L, 91L), + token = c( + "SYMBOL_FUNCTION_CALL", + "SYMBOL_FUNCTION_CALL" + ), + terminal = c(TRUE, TRUE), + text = c( + "sk_select_input", + "mod_test_2_module_ui" + ), + path = rep(path, 2), + is_input_output_funmodule = c(TRUE, TRUE), + is_followed_by = c("c", "ns"), + is_followed_by_ns = c(FALSE, TRUE) + ), + row.names = c(NA, -2L), + class = "data.frame" + ), + ignore_attr = TRUE + ) +}) From db841228de0199c373f14e0d32fe34d72664c47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Mon, 22 Jul 2024 18:27:46 +0200 Subject: [PATCH 11/16] test: create a test on a golem project --- tests/testthat/test-find_missing_ns.R | 128 ++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/tests/testthat/test-find_missing_ns.R b/tests/testthat/test-find_missing_ns.R index 6d8890a8..bb2e0637 100644 --- a/tests/testthat/test-find_missing_ns.R +++ b/tests/testthat/test-find_missing_ns.R @@ -244,3 +244,131 @@ test_that("Check check_namespace_in_file", { ignore_attr = TRUE ) }) + +dummy_dir_check_ns <- tempfile(pattern = "dummy") +dir.create(dummy_dir_check_ns) + +withr::with_dir(dummy_dir_check_ns, { + test_that("golem is created and properly populated", { + dummy_golem_path <- file.path(dummy_dir_check_ns, "checkns") + create_golem(dummy_golem_path, open = FALSE) + + expect_message( + checkns <- check_namespace_sanity( + pkg = dummy_golem_path + ), + "No shiny module found" + ) + + expect_false(checkns) + + file.create( + file.path(dummy_golem_path, "R", "mod_test_module.R") + ) + + writeLines( + c( + "#' first UI Function", + "#'", + "#' @description A shiny Module.", + "#'", + "#' @param id,input,output,session Internal parameters for {shiny}.", + "#'", + "#' @shinyModule A Golem module.", + "#'", + "#' @importFrom shiny NS tagList", + "mod_test_module_ui <- function(id) {", + " ns <- NS(id)", + " tagList(", + " selectInput(", + " inputId = ns('selectinput'),", + " label = 'Select input',", + " choices = c(LETTERS[1:10])", + " ),", + " actionButton(", + " inputId = ns('actionbutton'),", + " label = 'Action button'", + " )", + " )", + "}", + "", + "#' first Server Functions", + "#'", + "mod_test_module_server <- function(id) {", + " moduleServer(id, function(input, output, session) {", + " ns <- session$ns", + "", + " observeEvent(input$actionbutton, {", + " message(input$actionbutton)", + " message(input$selectinput)", + " })", + " })", + "}" + ), + con = file.path(dummy_golem_path, "R", "mod_test_module.R") + ) + + devtools::document(pkg = dummy_golem_path) + + expect_message( + checkns <- check_namespace_sanity( + pkg = dummy_golem_path + ), + "NS check passed" + ) + + expect_true(checkns) + + writeLines( + c( + "#' first UI Function", + "#'", + "#' @description A shiny Module.", + "#'", + "#' @param id,input,output,session Internal parameters for {shiny}.", + "#'", + "#' @shinyModule A Golem module.", + "#'", + "#' @importFrom shiny NS tagList", + "mod_test_module_ui <- function(id) {", + " ns <- NS(id)", + " tagList(", + " selectInput(", + " inputId = 'selectinput',", + " label = 'Select input',", + " choices = c(LETTERS[1:10])", + " ),", + " actionButton(", + " inputId = ns('actionbutton'),", + " label = 'Action button'", + " )", + " )", + "}", + "", + "#' first Server Functions", + "#'", + "mod_test_module_server <- function(id) {", + " moduleServer(id, function(input, output, session) {", + " ns <- session$ns", + "", + " observeEvent(input$actionbutton, {", + " message(input$actionbutton)", + " message(input$selectinput)", + " })", + " })", + "}" + ), + con = file.path(dummy_golem_path, "R", "mod_test_module.R") + ) + + expect_message( + checkns <- check_namespace_sanity( + pkg = dummy_golem_path, + ask_yesno = FALSE + ), + "It seems that..." + ) + + expect_true(checkns) + }) +}) From 543a2cf0642e4ee87a302a5e74c280a38e84cf90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Mon, 22 Jul 2024 18:28:36 +0200 Subject: [PATCH 12/16] chore: fix pkg path --- R/find_missing_ns.R | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/R/find_missing_ns.R b/R/find_missing_ns.R index 0a6cfb77..963934d4 100644 --- a/R/find_missing_ns.R +++ b/R/find_missing_ns.R @@ -75,9 +75,10 @@ check_namespace_in_file <- function( #' check namespace sanity #' Will check if the namespace (NS) are correctly set in the shiny modules #' -#' @param pkg The package path -#' @param extend_input_output_funmodule Extend the input, output or function module to check -#' @param disable Disable the check +#' @param pkg Character. The package path +#' @param extend_input_output_funmodule Character. Extend the input, output or function module to check +#' @param ask_yesno Logical. Ask the user to launch the app. Default is TRUE +#' @param disable Logical. Disable the check. Default is FALSE #' #' @importFrom roxygen2 parse_package block_get_tag #' @@ -87,6 +88,7 @@ check_namespace_in_file <- function( check_namespace_sanity <- function( pkg = golem::get_golem_wd(), extend_input_output_funmodule = NA_character_, + ask_yesno = TRUE, disable = FALSE ) { if (disable) { @@ -108,7 +110,7 @@ check_namespace_sanity <- function( } blocks <- roxygen2::parse_package( - path = ".", + path = base_path, env = NULL ) @@ -176,10 +178,13 @@ check_namespace_sanity <- function( purrr::walk(data$message, cli::cli_alert_danger) - launch_app <- yesno("Is it fixed? Do you want to launch the app?") - if (isFALSE(launch_app)) { - stop_quietly() + if (isTRUE(ask_yesno)) { + launch_app <- yesno("Is it fixed? Do you want to launch the app?") + + if (isFALSE(launch_app)) { + stop_quietly() + } } return(invisible(TRUE)) From 41898cac32e337a4a47831e398edd117bb14f1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Mon, 29 Jul 2024 18:07:15 +0200 Subject: [PATCH 13/16] feat: auto fix missing ns --- R/find_missing_ns.R | 98 +++++++++++++++------------ inst/shinyexample/dev/run_dev.R | 2 +- man/check_namespace_sanity.Rd | 8 +-- tests/testthat/test-find_missing_ns.R | 55 ++++++++++++--- 4 files changed, 105 insertions(+), 58 deletions(-) diff --git a/R/find_missing_ns.R b/R/find_missing_ns.R index 963934d4..ee972816 100644 --- a/R/find_missing_ns.R +++ b/R/find_missing_ns.R @@ -1,15 +1,12 @@ #' @noRd -stop_quietly <- function() { - opt <- options(show.error.messages = FALSE) - on.exit(options(opt)) - stop() +is_ns <- function(text) { + text == "ns" } #' @noRd is_shiny_input_output_funmodule <- function( - text, - extend_input_output_funmodule = NA_character_ -) { + text, + extend_input_output_funmodule = NA_character_) { stopifnot(is.character(extend_input_output_funmodule)) input_output_knew <- c("Input|Output|actionButton|radioButtons") @@ -39,12 +36,35 @@ is_shiny_input_output_funmodule <- function( ) } +#' @noRd +fix_ns_in_data <- function(data) { + for (i in 1:nrow(data)) { + line_index <- data$next_line1[i] + col_start <- data$next_col1[i] + col_end <- data$next_col2[i] + file <- data$path[i] + + file_content <- readLines(file) + + line_to_modify <- file_content[line_index] + modified_line <- paste0( + substr(line_to_modify, 1, col_start - 1), + "ns(", + substr(line_to_modify, col_start, col_end), + ")", + substr(line_to_modify, col_end + 1, nchar(line_to_modify)) + ) + file_content[line_index] <- modified_line + } + + writeLines(file_content, file) +} + #' @noRd #' @importFrom utils getParseData check_namespace_in_file <- function( - path, - extend_input_output_funmodule = NA_character_ -) { + path, + extend_input_output_funmodule = NA_character_) { getParseData( parse( file = path, @@ -55,17 +75,28 @@ check_namespace_in_file <- function( path = path ) |> dplyr::filter( - token == "SYMBOL_FUNCTION_CALL" + token %in% c( + "SYMBOL_FUNCTION_CALL", + "STR_CONST" + ) ) |> dplyr::mutate( is_input_output_funmodule = is_shiny_input_output_funmodule( text = text, extend_input_output_funmodule = extend_input_output_funmodule - ) - ) |> - dplyr::mutate( - is_followed_by = dplyr::lead(text), - is_followed_by_ns = is_followed_by == "ns" + ), + dplyr::across( + dplyr::starts_with("line"), + ~ dplyr::lead(.x), + .names = "next_{.col}" + ), + dplyr::across( + dplyr::starts_with("col"), + ~ dplyr::lead(.x), + .names = "next_{.col}" + ), + next_text = dplyr::lead(text), + is_followed_by_ns = is_ns(next_text) ) |> dplyr::filter( is_input_output_funmodule @@ -77,8 +108,7 @@ check_namespace_in_file <- function( #' #' @param pkg Character. The package path #' @param extend_input_output_funmodule Character. Extend the input, output or function module to check -#' @param ask_yesno Logical. Ask the user to launch the app. Default is TRUE -#' @param disable Logical. Disable the check. Default is FALSE +#' @param auto_fix Logical. Fix the missing namespace automatically. Default is TRUE #' #' @importFrom roxygen2 parse_package block_get_tag #' @@ -86,15 +116,9 @@ check_namespace_in_file <- function( #' #' @export check_namespace_sanity <- function( - pkg = golem::get_golem_wd(), - extend_input_output_funmodule = NA_character_, - ask_yesno = TRUE, - disable = FALSE -) { - if (disable) { - return(invisible(FALSE)) - } - + pkg = golem::get_golem_wd(), + extend_input_output_funmodule = NA_character_, + auto_fix = TRUE) { check_desc_installed() check_cli_installed() @@ -148,14 +172,6 @@ check_namespace_sanity <- function( dplyr::filter( !is_followed_by_ns ) |> - dplyr::select( - path, - text, - line1, - col1, - is_followed_by, - is_followed_by_ns - ) |> dplyr::mutate( message = sprintf("... see line %d in {.file %s:%d:%d}.", line1, path, line1, col1) ) @@ -174,17 +190,15 @@ check_namespace_sanity <- function( ) ) - cli::cli_alert_info("Fix {?this/these} {missing_ns_detected} missing namespace{?s} before to continue...") + cli::cli_alert_info("We recommand to fix {?this/these} {missing_ns_detected} missing namespace{?s} before to continue...") purrr::walk(data$message, cli::cli_alert_danger) - if (isTRUE(ask_yesno)) { - launch_app <- yesno("Is it fixed? Do you want to launch the app?") - - if (isFALSE(launch_app)) { - stop_quietly() - } + if (isTRUE(auto_fix)) { + fix_ns_in_data(data = data) + } else { + return(invisible(FALSE)) } return(invisible(TRUE)) diff --git a/inst/shinyexample/dev/run_dev.R b/inst/shinyexample/dev/run_dev.R index cb3afb91..05df559e 100644 --- a/inst/shinyexample/dev/run_dev.R +++ b/inst/shinyexample/dev/run_dev.R @@ -9,7 +9,7 @@ golem::detach_all_attached() # rm(list=ls(all.names = TRUE)) # Check for missing namespaces -golem::check_namespace_sanity(disable = FALSE) +golem::check_namespace_sanity(auto_fix = TRUE) # Document and reload your package golem::document_and_reload() diff --git a/man/check_namespace_sanity.Rd b/man/check_namespace_sanity.Rd index 67831335..694dc5ed 100644 --- a/man/check_namespace_sanity.Rd +++ b/man/check_namespace_sanity.Rd @@ -8,15 +8,15 @@ Will check if the namespace (NS) are correctly set in the shiny modules} check_namespace_sanity( pkg = golem::get_golem_wd(), extend_input_output_funmodule = NA_character_, - disable = FALSE + auto_fix = TRUE ) } \arguments{ -\item{pkg}{The package path} +\item{pkg}{Character. The package path} -\item{extend_input_output_funmodule}{Extend the input, output or function module to check} +\item{extend_input_output_funmodule}{Character. Extend the input, output or function module to check} -\item{disable}{Disable the check} +\item{auto_fix}{Logical. Fix the missing namespace automatically. Default is TRUE} } \value{ Logical. TRUE if the namespace are correctly set, FALSE otherwise diff --git a/tests/testthat/test-find_missing_ns.R b/tests/testthat/test-find_missing_ns.R index bb2e0637..58161b09 100644 --- a/tests/testthat/test-find_missing_ns.R +++ b/tests/testthat/test-find_missing_ns.R @@ -40,7 +40,6 @@ test_that("check is_shiny_input_output_funmodule works", { ) }) - test_that("Check check_namespace_in_file", { path <- tempfile(fileext = ".R") @@ -88,11 +87,24 @@ test_that("Check check_namespace_in_file", { "SYMBOL_FUNCTION_CALL" ), terminal = c(TRUE, TRUE), - text = c("selectInput", "actionButton"), + text = c( + "selectInput", + "actionButton" + ), path = rep(path, 2), - is_input_output_funmodule = c(TRUE, TRUE), # 2 TRUE - is_followed_by = c("c", "observeEvent"), - is_followed_by_ns = c(FALSE, FALSE) # 2 FALSE + is_input_output_funmodule = c(TRUE, TRUE), + next_line1 = c( + 5L, + 10L + ), + next_line2 = c(5L, 10L), + next_col1 = c(17L, 17L), + next_col2 = 29:30, + next_text = c( + "'selectinput'", + "'actionbutton'" + ), + is_followed_by_ns = c(FALSE, FALSE) ), row.names = c(NA, -2L), class = "data.frame" @@ -150,7 +162,11 @@ test_that("Check check_namespace_in_file", { ), path = rep(path, 2), is_input_output_funmodule = c(TRUE, TRUE), - is_followed_by = c("ns", "ns"), + next_line1 = c(5L, 10L), + next_line2 = c(5L, 10L), + next_col1 = c(17L, 17L), + next_col2 = c(18L, 18L), + next_text = c("ns", "ns"), is_followed_by_ns = c(TRUE, TRUE) ), row.names = c(NA, -2L), @@ -170,7 +186,7 @@ test_that("Check check_namespace_in_file", { " choices = c(LETTERS[1:10])", " ),", " mod_test_2_module_ui(", - " id = ns('actionbutton'),", # NS present + " id = ns('actionbutton')", # NS present " )", " )", "}", @@ -202,7 +218,11 @@ test_that("Check check_namespace_in_file", { text = "mod_test_2_module_ui", path = path, is_input_output_funmodule = TRUE, - is_followed_by = "ns", + next_line1 = 10L, + next_line2 = 10L, + next_col1 = 12L, + next_col2 = 13L, + next_text = "ns", is_followed_by_ns = TRUE ), row.names = c(NA, -1L), @@ -235,8 +255,14 @@ test_that("Check check_namespace_in_file", { ), path = rep(path, 2), is_input_output_funmodule = c(TRUE, TRUE), - is_followed_by = c("c", "ns"), - is_followed_by_ns = c(FALSE, TRUE) + next_line1 = c(5L, 10L), + next_line2 = c(5L, 10L), + next_col1 = c(17L, 12L), + next_col2 = c(29L, 13L), + next_text = c( + "'selectinput'", + "ns" + ), is_followed_by_ns = c(FALSE, TRUE) ), row.names = c(NA, -2L), class = "data.frame" @@ -364,11 +390,18 @@ withr::with_dir(dummy_dir_check_ns, { expect_message( checkns <- check_namespace_sanity( pkg = dummy_golem_path, - ask_yesno = FALSE + auto_fix = TRUE ), "It seems that..." ) expect_true(checkns) + + expect_message( + checkns <- check_namespace_sanity( + pkg = dummy_golem_path + ), + "NS check passed" + ) }) }) From 1fd9f812a0533f05a6198d30d65e761fca73a670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Mon, 29 Jul 2024 18:29:00 +0200 Subject: [PATCH 14/16] test: add more tests about automatic fix --- R/find_missing_ns.R | 17 ++--- tests/testthat/test-find_missing_ns.R | 106 ++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 9 deletions(-) diff --git a/R/find_missing_ns.R b/R/find_missing_ns.R index ee972816..baa4a8c3 100644 --- a/R/find_missing_ns.R +++ b/R/find_missing_ns.R @@ -5,8 +5,8 @@ is_ns <- function(text) { #' @noRd is_shiny_input_output_funmodule <- function( - text, - extend_input_output_funmodule = NA_character_) { + text, + extend_input_output_funmodule = NA_character_) { stopifnot(is.character(extend_input_output_funmodule)) input_output_knew <- c("Input|Output|actionButton|radioButtons") @@ -55,16 +55,15 @@ fix_ns_in_data <- function(data) { substr(line_to_modify, col_end + 1, nchar(line_to_modify)) ) file_content[line_index] <- modified_line + writeLines(file_content, file) } - - writeLines(file_content, file) } #' @noRd #' @importFrom utils getParseData check_namespace_in_file <- function( - path, - extend_input_output_funmodule = NA_character_) { + path, + extend_input_output_funmodule = NA_character_) { getParseData( parse( file = path, @@ -116,9 +115,9 @@ check_namespace_in_file <- function( #' #' @export check_namespace_sanity <- function( - pkg = golem::get_golem_wd(), - extend_input_output_funmodule = NA_character_, - auto_fix = TRUE) { + pkg = golem::get_golem_wd(), + extend_input_output_funmodule = NA_character_, + auto_fix = TRUE) { check_desc_installed() check_cli_installed() diff --git a/tests/testthat/test-find_missing_ns.R b/tests/testthat/test-find_missing_ns.R index 58161b09..63a54242 100644 --- a/tests/testthat/test-find_missing_ns.R +++ b/tests/testthat/test-find_missing_ns.R @@ -271,6 +271,70 @@ test_that("Check check_namespace_in_file", { ) }) +test_that("Check fix_ns_in_data works", { + path <- tempfile(fileext = ".R") + + writeLines( + c( + "mod_test_module_ui <- function(id) {", + " ns <- NS(id)", + " tagList(", + " selectInput(", + " inputId = 'selectinput',", # Missing NS + " label = 'Select input',", + " choices = c(LETTERS[1:10])", + " ),", + " actionButton(", + " inputId = 'actionbutton',", # Missing NS + " label = 'Action button'", + " )", + " )", + "}", + "", + "mod_test_module_server <- function(id) {", + " observeEvent(input$actionbutton, {", + " message(input$actionbutton)", + " message(input$selectinput)", + " })", + "}" + ), + con = path + ) + + data <- check_namespace_in_file( + path = path + ) + + fix_ns_in_data(data) + + expect_equal( + readLines(path), + c( + "mod_test_module_ui <- function(id) {", + " ns <- NS(id)", + " tagList(", + " selectInput(", + " inputId = ns('selectinput'),", # NS present + " label = 'Select input',", + " choices = c(LETTERS[1:10])", + " ),", + " actionButton(", + " inputId = ns('actionbutton'),", # NS present + " label = 'Action button'", + " )", + " )", + "}", + "", + "mod_test_module_server <- function(id) {", + " observeEvent(input$actionbutton, {", + " message(input$actionbutton)", + " message(input$selectinput)", + " })", + "}" + ) + ) +}) + dummy_dir_check_ns <- tempfile(pattern = "dummy") dir.create(dummy_dir_check_ns) @@ -403,5 +467,47 @@ withr::with_dir(dummy_dir_check_ns, { ), "NS check passed" ) + + expect_equal( + readLines(file.path(dummy_golem_path, "R", "mod_test_module.R")), + c( + "#' first UI Function", + "#'", + "#' @description A shiny Module.", + "#'", + "#' @param id,input,output,session Internal parameters for {shiny}.", + "#'", + "#' @shinyModule A Golem module.", + "#'", + "#' @importFrom shiny NS tagList", + "mod_test_module_ui <- function(id) {", + " ns <- NS(id)", + " tagList(", + " selectInput(", + " inputId = ns('selectinput'),", + " label = 'Select input',", + " choices = c(LETTERS[1:10])", + " ),", + " actionButton(", + " inputId = ns('actionbutton'),", + " label = 'Action button'", + " )", + " )", + "}", + "", + "#' first Server Functions", + "#'", + "mod_test_module_server <- function(id) {", + " moduleServer(id, function(input, output, session) {", + " ns <- session$ns", + "", + " observeEvent(input$actionbutton, {", + " message(input$actionbutton)", + " message(input$selectinput)", + " })", + " })", + "}" + ) + ) }) }) From 625934a919d52e759f71f8f6d0dea766cfb26715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Mon, 29 Jul 2024 18:36:37 +0200 Subject: [PATCH 15/16] feat: add more context --- R/find_missing_ns.R | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/R/find_missing_ns.R b/R/find_missing_ns.R index baa4a8c3..02eed311 100644 --- a/R/find_missing_ns.R +++ b/R/find_missing_ns.R @@ -5,8 +5,8 @@ is_ns <- function(text) { #' @noRd is_shiny_input_output_funmodule <- function( - text, - extend_input_output_funmodule = NA_character_) { + text, + extend_input_output_funmodule = NA_character_) { stopifnot(is.character(extend_input_output_funmodule)) input_output_knew <- c("Input|Output|actionButton|radioButtons") @@ -62,8 +62,8 @@ fix_ns_in_data <- function(data) { #' @noRd #' @importFrom utils getParseData check_namespace_in_file <- function( - path, - extend_input_output_funmodule = NA_character_) { + path, + extend_input_output_funmodule = NA_character_) { getParseData( parse( file = path, @@ -115,9 +115,9 @@ check_namespace_in_file <- function( #' #' @export check_namespace_sanity <- function( - pkg = golem::get_golem_wd(), - extend_input_output_funmodule = NA_character_, - auto_fix = TRUE) { + pkg = golem::get_golem_wd(), + extend_input_output_funmodule = NA_character_, + auto_fix = TRUE) { check_desc_installed() check_cli_installed() @@ -195,7 +195,9 @@ check_namespace_sanity <- function( if (isTRUE(auto_fix)) { + cli::cli_process_start("`auto_fix` is TRUE so we will fix the missing namespace") fix_ns_in_data(data = data) + cli::cli_process_done() } else { return(invisible(FALSE)) } From bc15e2dd102488d2623d2e5b7d5254111e98933d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Br=C3=A9ant?= Date: Tue, 30 Jul 2024 11:59:18 +0200 Subject: [PATCH 16/16] feat: check for urls validity --- R/find_invalid_url.R | 66 +++++++++++++++++++++++++++++++++ inst/shinyexample/dev/run_dev.R | 3 ++ 2 files changed, 69 insertions(+) create mode 100644 R/find_invalid_url.R diff --git a/R/find_invalid_url.R b/R/find_invalid_url.R new file mode 100644 index 00000000..d297f741 --- /dev/null +++ b/R/find_invalid_url.R @@ -0,0 +1,66 @@ +#' @noRd +extract_urls_from_file <- function(file) { + content <- readLines(file, warn = FALSE) + url_pattern <- "(http|https)://[a-zA-Z0-9./?=_-]*" + urls <- unique(unlist(regmatches(content, gregexpr(url_pattern, content)))) + names(urls) <- rep(file, length(urls)) + return(urls) +} + +#' @noRd +check_url <- function(url) { + response <- try(httr::GET(url), silent = TRUE) + if (inherits(response, "try-error")) { + res <- FALSE + } + + res <- httr::status_code(response) == 200 + res <- stats::setNames(res, url) + + return(res) +} + +#' Check for the validity of the URLs in the R folder +#' +#' @param exclude a character vector of urls to exclude +#' +#' @return a message if some URLs are invalid +#' @export +check_url_validity <- function(exclude = NA_character_) { + if (!curl::has_internet()) { + cli::cli_alert_info("No internet connection.") + return(invisible(FALSE)) + } + + if (!dir.exists("R")) { + cli::cli_alert_info("No R folder found.") + return(invisible(FALSE)) + } + + urls <- purrr::map( + files, + ~ extract_urls_from_file(file = .x) + ) |> + unlist() |> + purrr::discard( + ~ .x %in% exclude + ) |> + purrr::map( + check_url + ) + + invalid_urls <- urls |> + purrr::keep( + ~ .x == FALSE + ) + + if (length(invalid_urls) > 0) { + cli::cli_alert_info("Some URLs are invalid.") + purrr::walk( + invalid_urls, + ~ cli::cli_alert_danger(sprintf("URL %s is invalid in file {.file %s}", names(.x), names(invalid_urls))) + ) + } else { + cli::cli_alert_success("All URLs are valid.") + } +} diff --git a/inst/shinyexample/dev/run_dev.R b/inst/shinyexample/dev/run_dev.R index 05df559e..63e5db4d 100644 --- a/inst/shinyexample/dev/run_dev.R +++ b/inst/shinyexample/dev/run_dev.R @@ -8,6 +8,9 @@ options(shiny.port = httpuv::randomPort()) golem::detach_all_attached() # rm(list=ls(all.names = TRUE)) +# Check for invalid URLs +golem::check_url_validity() + # Check for missing namespaces golem::check_namespace_sanity(auto_fix = TRUE)