diff --git a/DESCRIPTION b/DESCRIPTION index 4006321..ffb3e86 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: box.linters Title: Linters for 'box' Modules -Version: 0.9.0.9004 +Version: 0.9.0.9005 Authors@R: c( person("Ricardo Rodrigo", "Basa", role = c("aut", "cre"), email = "opensource+rodrigo@appsilon.com"), @@ -23,7 +23,9 @@ Imports: glue, lintr (>= 3.0.0), rlang, - xml2 + stringr, + xml2, + xmlparsedata Suggests: box, covr, @@ -36,10 +38,8 @@ Suggests: rhino, shiny, spelling, - stringr, testthat (>= 3.0.0), withr, - xmlparsedata Config/testthat/edition: 3 Config/testthat/parallel: true Language: en-US diff --git a/NEWS.md b/NEWS.md index 14fc233..546d921 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # box.linters (development version) +* Added handler for `glue` string templates. * Added box_mod_fun_exists_linter() to default linters * [bug fix] box_trailing_commas_linter() now properly handles a #nolint for other linters * [bug fix] `box_unused_att_pkg_fun_linter()` had issues with lists of functions. Linter function diff --git a/R/box_unused_attached_mod_linter.R b/R/box_unused_attached_mod_linter.R index 5ab3d30..903d0ce 100644 --- a/R/box_unused_attached_mod_linter.R +++ b/R/box_unused_attached_mod_linter.R @@ -82,6 +82,9 @@ box_unused_attached_mod_linter <- function() { attached_modules <- get_attached_modules(xml) attached_three_dots <- get_attached_mod_three_dots(xml) function_calls <- get_function_calls(xml) + glue_object_calls <- get_objects_in_strings(xml) + + all_calls_text <- c(function_calls$text, glue_object_calls) unused_module <- lapply(attached_modules$xml, function(attached_module) { module_text <- basename(lintr::get_r_string(attached_module)) @@ -93,7 +96,7 @@ box_unused_attached_mod_linter <- function() { sep = "$" ) - functions_used <- length(intersect(func_list, function_calls$text)) + functions_used <- length(intersect(func_list, all_calls_text)) if (functions_used == 0) { lintr::xml_nodes_to_lints( @@ -109,7 +112,7 @@ box_unused_attached_mod_linter <- function() { module_text <- basename(lintr::get_r_string(attached_module)) module_text <- sub("\\[\\.\\.\\.\\]", "", module_text) func_list <- attached_three_dots$nested[[module_text]] - functions_used <- length(intersect(func_list, function_calls$text)) + functions_used <- length(intersect(func_list, all_calls_text)) if (functions_used == 0) { lintr::xml_nodes_to_lints( diff --git a/R/box_unused_attached_mod_obj_linter.R b/R/box_unused_attached_mod_obj_linter.R index deddc15..8d6874c 100644 --- a/R/box_unused_attached_mod_obj_linter.R +++ b/R/box_unused_attached_mod_obj_linter.R @@ -64,7 +64,8 @@ box_unused_att_mod_obj_linter <- function() { attached_functions_objects <- get_attached_mod_functions(xml) function_calls <- get_function_calls(xml) object_calls <- get_object_calls(xml) - all_object_calls_text <- c(function_calls$text, object_calls$text) + glue_object_calls <- get_objects_in_strings(xml) + all_object_calls_text <- c(function_calls$text, object_calls$text, glue_object_calls) lapply(attached_functions_objects$xml, function(fun_obj_import) { fun_obj_import_text <- xml2::xml_text(fun_obj_import) diff --git a/R/box_unused_attached_pkg_fun_linter.R b/R/box_unused_attached_pkg_fun_linter.R index f25580e..d21a380 100644 --- a/R/box_unused_attached_pkg_fun_linter.R +++ b/R/box_unused_attached_pkg_fun_linter.R @@ -69,12 +69,15 @@ box_unused_att_pkg_fun_linter <- function() { FUN.VALUE = character(1) ) + glue_object_calls <- get_objects_in_strings(xml) + all_calls_text <- c(function_calls$text, glue_object_calls) + lapply(attached_functions$xml, function(fun_import) { fun_import_text <- xml2::xml_text(fun_import) fun_import_text <- gsub("[`'\"]", "", fun_import_text) aliased_fun_import_text <- attached_functions$text[fun_import_text] - if (!aliased_fun_import_text %in% function_calls$text) { + if (!aliased_fun_import_text %in% all_calls_text) { lintr::xml_nodes_to_lints( fun_import, source_expression = source_expression, diff --git a/R/box_unused_attached_pkg_linter.R b/R/box_unused_attached_pkg_linter.R index a58b529..590c65d 100644 --- a/R/box_unused_attached_pkg_linter.R +++ b/R/box_unused_attached_pkg_linter.R @@ -80,6 +80,9 @@ box_unused_attached_pkg_linter <- function() { attached_packages <- get_attached_packages(xml) attached_three_dots <- get_attached_pkg_three_dots(xml) function_calls <- get_function_calls(xml) + glue_object_calls <- get_objects_in_strings(xml) + + all_calls_text <- c(function_calls$text, glue_object_calls) unused_package <- lapply(attached_packages$xml, function(attached_package) { package_text <- lintr::get_r_string(attached_package) @@ -91,7 +94,7 @@ box_unused_attached_pkg_linter <- function() { sep = "$" ) - functions_used <- length(intersect(func_list, function_calls$text)) + functions_used <- length(intersect(func_list, all_calls_text)) if (functions_used == 0) { lintr::xml_nodes_to_lints( @@ -106,7 +109,7 @@ box_unused_attached_pkg_linter <- function() { unused_three_dots <- lapply(attached_three_dots$xml, function(attached_package) { package_text <- lintr::get_r_string(attached_package) func_list <- attached_three_dots$nested[[package_text]] - functions_used <- length(intersect(func_list, function_calls$text)) + functions_used <- length(intersect(func_list, all_calls_text)) if (functions_used == 0) { lintr::xml_nodes_to_lints( diff --git a/R/box_usage_helper_functions.R b/R/box_usage_helper_functions.R index db9ebb0..d1c5e4d 100644 --- a/R/box_usage_helper_functions.R +++ b/R/box_usage_helper_functions.R @@ -109,12 +109,13 @@ get_object_calls <- function(xml) { xpath_all_object_calls <- " .//expr[ + ./SYMBOL and not( following-sibling::LEFT_ASSIGN or following-sibling::EQ_ASSIGN ) ] - /SYMBOL" + " xml_object_calls <- xml2::xml_find_all(xml_no_box_use, xpath_all_object_calls) text <- xml2::xml_text(xml_object_calls, trim = TRUE) diff --git a/R/get_objects_in_strings.R b/R/get_objects_in_strings.R new file mode 100644 index 0000000..a9f125f --- /dev/null +++ b/R/get_objects_in_strings.R @@ -0,0 +1,41 @@ +#' Get objects used in `glue` string templates +#' +#' @description +#' In `glue`, all text between `{` and `}` is considered code. Literal braces are defined as +#' `{{` and `}}`. Text between double braces are not interpolated. +#' +#' @param xml An xml node list. +#' @return A character vector of object and function names found inside `glue` string templates. +#' @keywords internal +get_objects_in_strings <- function(xml) { + xpath_str_consts <- " + //expr/STR_CONST + " + + all_strings <- extract_xml_and_text(xml, xpath_str_consts) + text_between_braces <- stringr::str_match_all(all_strings$text, "(\\{(?:\\{??[^\\{]*?\\}))") + + tryCatch({ + unlist( + lapply(text_between_braces, function(each_text) { + if (identical(each_text[, 2], character(0))) { + return(NULL) + } + + parsed_code <- parse(text = each_text[, 2], keep.source = TRUE) + xml_parsed_code <- xml2::read_xml(xmlparsedata::xml_parse_data(parsed_code)) + objects_called <- get_object_calls(xml_parsed_code) + functions_calls <- get_function_calls(xml_parsed_code) + + return( + c( + objects_called$text, + functions_calls$text + ) + ) + }) + ) + }, error = function(e) { + return(NULL) + }) +} diff --git a/R/unused_declared_object_linter.R b/R/unused_declared_object_linter.R index 10e0392..a46eda5 100644 --- a/R/unused_declared_object_linter.R +++ b/R/unused_declared_object_linter.R @@ -58,7 +58,8 @@ unused_declared_object_linter <- function() { exported_objects <- get_exported_objects(xml) function_calls <- get_function_calls(xml) object_calls <- get_object_calls(xml) - local_calls_text <- c(function_calls$text, object_calls$text) + glue_object_calls <- get_objects_in_strings(xml) + local_calls_text <- c(function_calls$text, object_calls$text, glue_object_calls) lapply(fun_assignments$xml_nodes, function(fun_assign) { fun_assign_text <- xml2::xml_text(fun_assign) diff --git a/man/get_objects_in_strings.Rd b/man/get_objects_in_strings.Rd new file mode 100644 index 0000000..d40c728 --- /dev/null +++ b/man/get_objects_in_strings.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get_objects_in_strings.R +\name{get_objects_in_strings} +\alias{get_objects_in_strings} +\title{Get objects used in \code{glue} string templates} +\usage{ +get_objects_in_strings(xml) +} +\arguments{ +\item{xml}{An xml node list.} +} +\value{ +A character vector of object and function names found inside \code{glue} string templates. +} +\description{ +In \code{glue}, all text between \verb{\{} and \verb{\}} is considered code. Literal braces are defined as +\verb{\{\{} and \verb{\}\}}. Text between double braces are not interpolated. +} +\keyword{internal} diff --git a/tests/testthat/test-box_unused_attached_mod_linter.R b/tests/testthat/test-box_unused_attached_mod_linter.R index 59899f9..738662c 100644 --- a/tests/testthat/test-box_unused_attached_mod_linter.R +++ b/tests/testthat/test-box_unused_attached_mod_linter.R @@ -124,3 +124,148 @@ test_that("box_unused_mod_linter blocks unused three-dots attached packages", { lintr::expect_lint(bad_box_usage, list(message = lint_message), linter) }) + + +# Glue compatibility + +test_that("box_unused_attached_mod_linter skips objects used in glue string templates", { + linter <- box_unused_attached_mod_linter() + + good_box_usage <- "box::use( + glue, + ) + + box::use( + path/to/module_b, + ) + + glue$glue(\"This {module_b$b_obj_a} should be parsed.\") + " + + lintr::expect_lint(good_box_usage, NULL, linters = linter) +}) + +test_that("box_unused_attached_mod_linter skips functions used in glue string templates", { + linter <- box_unused_attached_mod_linter() + + good_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b, + ) + + glue(\"This {module_b$b_fun_a()} should be parsed.\") + " + + lintr::expect_lint(good_box_usage, NULL, linters = linter) +}) + +test_that("box_unused_attached_mod_linter skips literal braces in glue string templates", { + linter <- box_unused_attached_mod_linter() + lint_message <- rex::rex("Attached module unused.") + + bad_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b, + ) + + glue(\"This {{module_b$b_obj_a}} should be parsed.\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message), linters = linter) +}) + +test_that("box_unused_attached_mod_linter blocks unused objects in glue string templates", { + linter <- box_unused_attached_mod_linter() + lint_message <- rex::rex("Attached module unused.") + + bad_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b, + ) + + glue(\"This does not have a parseable object.\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message), linters = linter) +}) + +# Glue compatibility three dots + +test_that("box_unused_attached_mod_linter skips objects used in glue string templates", { + linter <- box_unused_attached_mod_linter() + + good_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b[...], + ) + + glue(\"This {b_obj_a} should be parsed.\") + " + + lintr::expect_lint(good_box_usage, NULL, linters = linter) +}) + +test_that("box_unused_attached_mod_linter skips functions used in glue string templates", { + linter <- box_unused_attached_mod_linter() + + good_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b[...], + ) + + glue(\"This {b_fun_a()} should be parsed.\") + " + + lintr::expect_lint(good_box_usage, NULL, linters = linter) +}) + +test_that("box_unused_attached_mod_linter skips literal braces in glue string templates", { + linter <- box_unused_attached_mod_linter() + lint_message <- rex::rex("Three-dots attached module unused.") + + bad_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b[...], + ) + + glue(\"This {{b_obj_a}} should be parsed.\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message), linters = linter) +}) + +test_that("box_unused_attached_mod_linter blocks unused objects in glue string templates", { + linter <- box_unused_attached_mod_linter() + lint_message <- rex::rex("Three-dots attached module unused.") + + bad_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b[...], + ) + + glue(\"This does not have a parseable object.\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message), linters = linter) +}) diff --git a/tests/testthat/test-box_unused_attached_mod_obj_linter.R b/tests/testthat/test-box_unused_attached_mod_obj_linter.R index 3388ac9..709b11e 100644 --- a/tests/testthat/test-box_unused_attached_mod_obj_linter.R +++ b/tests/testthat/test-box_unused_attached_mod_obj_linter.R @@ -139,3 +139,73 @@ test_that("box_unused_att_mod_obj_linter blocks used function in list", { lintr::expect_lint(bad_box_usage, list(message = lint_message), linter) }) + +test_that("box_unused_att_mod_obj_linter skips objects used in glue string templates", { + linter <- box_unused_att_mod_obj_linter() + + good_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b[b_obj_a], + ) + + glue(\"This {b_obj_a} should be parsed.\") + " + + lintr::expect_lint(good_box_usage, NULL, linters = linter) +}) + +test_that("box_unused_att_mod_obj_linter skips functions used in glue string templates", { + linter <- box_unused_att_mod_obj_linter() + + good_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b[b_fun_a], + ) + + glue(\"This {b_fun_a()} should be parsed.\") + " + + lintr::expect_lint(good_box_usage, NULL, linters = linter) +}) + +test_that("box_unused_att_mod_obj_linter skips literal braces in glue string templates", { + linter <- box_unused_att_mod_obj_linter() + lint_message <- rex::rex("Imported function/object unused.") + + bad_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b[b_obj_a], + ) + + glue(\"This {{b_obj_a}} should be parsed.\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message), linters = linter) +}) + +test_that("box_unused_att_mod_obj_linter blocks unused objects in glue string templates", { + linter <- box_unused_att_mod_obj_linter() + lint_message <- rex::rex("Imported function/object unused.") + + bad_box_usage <- "box::use( + glue[glue], + ) + + box::use( + path/to/module_b[b_obj_a], + ) + + glue(\"This does not have a parseable object.\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message), linters = linter) +}) diff --git a/tests/testthat/test-box_unused_attached_pkg_fun_linter.R b/tests/testthat/test-box_unused_attached_pkg_fun_linter.R index eac5f4e..e202339 100644 --- a/tests/testthat/test-box_unused_attached_pkg_fun_linter.R +++ b/tests/testthat/test-box_unused_attached_pkg_fun_linter.R @@ -119,3 +119,50 @@ test_that("box_unused_att_pkg_fun_linter blocks unused function in list", { lintr::expect_lint(bad_box_usage, list(message = lint_message), linter) }) + +test_that("box_unused_att_pkg_fun_linter skips function used in glue string template", { + linter <- box_unused_att_pkg_fun_linter() + + good_box_usage <- "box::use( + glue[glue], + stringr[str_trim], + ) + + string_with_spaces <- \" String with white spaces\t\" + glue(\"This {str_trim(string_with_spaces)} should be parsed\") + " + + lintr::expect_lint(good_box_usage, NULL, linters = linter) +}) + +test_that("box_unused_att_pkg_fun_linter skips literal braces in glue string template", { + linter <- box_unused_att_pkg_fun_linter() + lint_message_1 <- rex::rex("Imported function unused.") + + bad_box_usage <- "box::use( + glue[glue], + stringr[str_trim], + ) + + string_with_spaces <- \" String with white spaces\t\" + glue(\"This {{str_trim(string_with_spaces)}} should be parsed\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message_1), linters = linter) +}) + +test_that("box_unused_att_pkg_fun_linter blocks unused functions in glue string template", { + linter <- box_unused_att_pkg_fun_linter() + lint_message_1 <- rex::rex("Imported function unused.") + + bad_box_usage <- "box::use( + glue[glue], + stringr[str_trim], + ) + + string_with_spaces <- \" String with white spaces\t\" + glue(\"This does not have a pareable object.\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message_1), linters = linter) +}) diff --git a/tests/testthat/test-box_unused_attached_pkg_linter.R b/tests/testthat/test-box_unused_attached_pkg_linter.R index ffebda7..87a8f3d 100644 --- a/tests/testthat/test-box_unused_attached_pkg_linter.R +++ b/tests/testthat/test-box_unused_attached_pkg_linter.R @@ -116,3 +116,102 @@ test_that("box_unused_pkg_linter blocks unused three-dots attached packages", { lintr::expect_lint(bad_box_usage, list(message = lint_message), linter) }) + + +# Glue compatiblity + +test_that("box_unused_attached_pkg_linter skips function used in glue string template", { + linter <- box_unused_attached_pkg_linter() + + good_box_usage <- "box::use( + glue, + stringr, + ) + + string_with_spaces <- \" String with white spaces\t\" + glue$glue(\"This {stringr$str_trim(string_with_spaces)} should be parsed\") + " + + lintr::expect_lint(good_box_usage, NULL, linters = linter) +}) + +test_that("box_unused_attached_pkg_linter skips literal braces in glue string template", { + linter <- box_unused_attached_pkg_linter() + lint_message_1 <- rex::rex("Attached package unused.") + + bad_box_usage <- "box::use( + glue, + stringr, + ) + + string_with_spaces <- \" String with white spaces\t\" + glue$glue(\"This {{stringr$str_trim(string_with_spaces)}} should be parsed\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message_1), linters = linter) +}) + +test_that("box_unused_attached_pkg_linter blocks unused functions in glue string template", { + linter <- box_unused_attached_pkg_linter() + lint_message_1 <- rex::rex("Attached package unused.") + + bad_box_usage <- "box::use( + glue, + stringr, + ) + + string_with_spaces <- \" String with white spaces\t\" + glue$glue(\"This does not have a pareable object.\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message_1), linters = linter) +}) + +# Glue compatibility three dots + +test_that("box_unused_attached_pkg_linter skips function used in glue string template", { + linter <- box_unused_attached_pkg_linter() + + good_box_usage <- "box::use( + glue[...], + stringr[...], + ) + + string_with_spaces <- \" String with white spaces\t\" + glue(\"This {str_trim(string_with_spaces)} should be parsed\") + " + + lintr::expect_lint(good_box_usage, NULL, linters = linter) +}) + +test_that("box_unused_attached_pkg_linter skips literal braces in glue string template", { + linter <- box_unused_attached_pkg_linter() + lint_message_1 <- rex::rex("Three-dots attached package unused.") + + bad_box_usage <- "box::use( + glue[...], + stringr[...], + ) + + string_with_spaces <- \" String with white spaces\t\" + glue(\"This {{str_trim(string_with_spaces)}} should be parsed\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message_1), linters = linter) +}) + +test_that("box_unused_attached_pkg_linter blocks unused functions in glue string template", { + linter <- box_unused_attached_pkg_linter() + lint_message_1 <- rex::rex("Three-dots attached package unused.") + + bad_box_usage <- "box::use( + glue[...], + stringr[...], + ) + + string_with_spaces <- \" String with white spaces\t\" + glue(\"This does not have a pareable object.\") + " + + lintr::expect_lint(bad_box_usage, list(message = lint_message_1), linters = linter) +}) diff --git a/tests/testthat/test-box_usage_helper_functions.R b/tests/testthat/test-box_usage_helper_functions.R index 59e585d..991ebd1 100644 --- a/tests/testthat/test-box_usage_helper_functions.R +++ b/tests/testthat/test-box_usage_helper_functions.R @@ -37,7 +37,21 @@ obj_a <- obj_b result <- get_function_calls(xml_function_calls) expected_result <- c("fun_a", "fun_b") - expect_equal(result$text, expected_result) + expect_setequal(result$text, expected_result) +}) + +test_that("get_function_calls returns correct list of function calls", { + function_calls <- " +container$fun_a() +another$fun_b(1, 2) +obj_a <- obj_b +" + + xml_function_calls <- code_to_xml_expr(function_calls) + result <- get_function_calls(xml_function_calls) + expected_result <- c("container$fun_a", "another$fun_b") + + expect_setequal(result$text, expected_result) }) test_that("get_declared_objects returns correct list of object definitions", { @@ -69,6 +83,19 @@ test_that("get_object_calls returns correct list of object calls with equal assi expect_equal(result$text, expected_result) }) +test_that("get_object_calls returns list objects", { + object_list_calls <- " + sum(container$object) + mean(another$object) + " + + xml_object_calls <- code_to_xml_expr(object_list_calls) + result <- get_object_calls(xml_object_calls) + expected_result <- c("container$object", "another$object", "container", "another") + + expect_setequal(result$text, expected_result) +}) + test_that("get_object_calls returns objects passed to functions", { object_calls <- " obj_a <- 5 diff --git a/tests/testthat/test-get_objects_in_strings.R b/tests/testthat/test-get_objects_in_strings.R new file mode 100644 index 0000000..bed2e5b --- /dev/null +++ b/tests/testthat/test-get_objects_in_strings.R @@ -0,0 +1,130 @@ +code_to_xml_expr <- function(text_code) { + xml2::read_xml( + xmlparsedata::xml_parse_data( + parse(text = text_code, keep.source = TRUE) + ) + ) +} + +test_that("get_objects_in_strings extracts a single variable name", { + code <- " + string <- \"Some {value} in a string.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- "value" + + expect_equal(results, should_find) +}) + +test_that("get_objects_in_strings extracts multiple variable names", { + code <- " + string <- \"Some {value_a} and {value_b} in a string.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- c("value_a", "value_b") + + expect_setequal(results, should_find) + + code <- " + string <- \"Some {value_a}, {value_b}, and {value_c} in a string.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- c("value_a", "value_b", "value_c") + + expect_setequal(results, should_find) +}) + +test_that("get_objects_in_strings extracts function names", { + code <- " + string <- \"Some {func()} in a string.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- "func" + + expect_equal(results, should_find) +}) + +test_that("get_objects_in_strings extracts function names and parameter names", { + code <- " + string <- \"Some {func(value)} in a string.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- c("func", "value") + + expect_setequal(results, should_find) + + code <- " + string <- \"Some {func(value_a, value_b)} in a string.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- c("func", "value_a", "value_b") + + expect_setequal(results, should_find) +}) + +test_that("get_objects_in_strings ignores other elements", { + code <- " + string <- \"Some {value + 1} in a string.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- "value" + + expect_equal(results, should_find) +}) + +test_that("get_objects_in_strings ignores literal glue objects {{ }}", { + code <- " + string <- \"Some {{value + 1}} in a string.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + + expect_equal(results, NULL) +}) + +test_that("get_objects_in_strings extracts objects from multiline code", { + code <- " + string <- \"Some text { + { + internal_var <- external_var + some_function_call(internal_var, another_external_var) + } + } here.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- c("external_var", "some_function_call", "internal_var", "another_external_var") + + expect_setequal(results, should_find) +}) + +test_that("get_objects_in_strings handles multiple string constants in code", { + code <- " + string_1 <- \"Some text {value_a} here.\" + string_2 <- \"Some text {value_b} here.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- c("value_a", "value_b") + + expect_setequal(results, should_find) +}) + +test_that("get_objects_in_strings handles multiple string constants in code", { + code <- " + string_1 <- \"No parseable object here.\" + string_2 <- \"Some text {value_b} here.\" + " + xml_code <- code_to_xml_expr(code) + results <- get_objects_in_strings(xml_code) + should_find <- c("value_b") + + expect_setequal(results, should_find) +}) diff --git a/tests/testthat/test-unused_declared_object_linter.R b/tests/testthat/test-unused_declared_object_linter.R index 567dcf4..6ee8ece 100644 --- a/tests/testthat/test-unused_declared_object_linter.R +++ b/tests/testthat/test-unused_declared_object_linter.R @@ -202,3 +202,65 @@ test_that("unused_declared_object_linter skips assignment to list elements", { lintr::expect_lint(path_to_shiny_app, NULL, linters = linter) }) + +test_that("unused_declared_object_linter skips valid objects called in glue string templates", { + linter <- unused_declared_object_linter() + + code <- " + box::use( + glue[glue], + ) + + some_value <- 4 + glue(\"This {some_value} should be parsed.\") + " + + lintr::expect_lint(code, NULL, linters = linter) +}) + +test_that("unused_declared_object_linter skips valid objects called in glue string templates", { + linter <- unused_declared_object_linter() + + code <- " + box::use( + glue[glue], + ) + + some_func <- function() { + 4 + } + glue(\"This {some_func()} should be parsed.\") + " + + lintr::expect_lint(code, NULL, linters = linter) +}) + +test_that("unused_declared_object_linter skips literal braces in glue string templates", { + linter <- unused_declared_object_linter() + + code <- " + box::use( + glue[glue], + ) + + glue(\"This {{literal_braces}} should be parsed.\") + " + + lintr::expect_lint(code, NULL, linters = linter) +}) + +test_that("unused_declared_object_linter blocks unused objects in glue string templates", { + linter <- unused_declared_object_linter() + lint_message <- rex::rex("Declared function/object unused.") + + code <- " + box::use( + glue[glue], + ) + + some_value <- 4 + glue(\"This does not have a parseable object.\") + " + + lintr::expect_lint(code, list(message = lint_message), linters = linter) +})