diff --git a/.gitignore b/.gitignore index eedcebdf..7096c5c0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ tmp.* vignettes/*.R vignettes/*.html vignettes/*.md +tests/testthat/Rplots.pdf diff --git a/DESCRIPTION b/DESCRIPTION index fddcb600..e3c8d848 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -61,4 +61,5 @@ Collate: 'qenv-show.R' 'qenv-within.R' 'teal.code-package.R' + 'utils-code_dependency.R' 'utils.R' diff --git a/NEWS.md b/NEWS.md index a2d37579..fdf1b7a5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # teal.code 0.4.1.9001 +* The `@code` field in the `qenv` class now holds `character`, not `expression`. + # teal.code 0.4.1 ### Miscellaneous diff --git a/R/qenv-class.R b/R/qenv-class.R index a2e603ab..b664840d 100644 --- a/R/qenv-class.R +++ b/R/qenv-class.R @@ -3,7 +3,7 @@ #' Reproducible class with environment and code. #' @name qenv-class #' @rdname qenv-class -#' @slot code (`expression`) to reproduce the environment +#' @slot code (`character`) representing code necessary to reproduce the environment #' @slot env (`environment`) environment which content was generated by the evaluation #' of the `code` slot. #' @slot id (`integer`) random identifier of the code element to make sure uniqueness @@ -13,9 +13,9 @@ #' @keywords internal setClass( "qenv", - slots = c(env = "environment", code = "expression", id = "integer", warnings = "character", messages = "character"), + slots = c(env = "environment", code = "character", id = "integer", warnings = "character", messages = "character"), prototype = list( - env = new.env(parent = parent.env(.GlobalEnv)), code = expression(), id = integer(0), + env = new.env(parent = parent.env(.GlobalEnv)), code = character(0), id = integer(0), warnings = character(0), messages = character(0) ) ) diff --git a/R/qenv-constructor.R b/R/qenv-constructor.R index 45e34e16..694623a8 100644 --- a/R/qenv-constructor.R +++ b/R/qenv-constructor.R @@ -10,13 +10,13 @@ #' #' @examples #' new_qenv(env = list2env(list(a = 1)), code = quote(a <- 1)) -#' new_qenv(env = list2env(list(a = 1)), code = parse(text = "a <- 1")) +#' new_qenv(env = list2env(list(a = 1)), code = parse(text = "a <- 1", keep.source = TRUE)) #' new_qenv(env = list2env(list(a = 1)), code = "a <- 1") #' #' @return `qenv` object. #' #' @export -setGeneric("new_qenv", function(env = new.env(parent = parent.env(.GlobalEnv)), code = expression()) standardGeneric("new_qenv")) # nolint +setGeneric("new_qenv", function(env = new.env(parent = parent.env(.GlobalEnv)), code = character()) standardGeneric("new_qenv")) # nolint #' @rdname new_qenv #' @export @@ -24,13 +24,7 @@ setMethod( "new_qenv", signature = c(env = "environment", code = "expression"), function(env, code) { - new_env <- rlang::env_clone(env, parent = parent.env(.GlobalEnv)) - lockEnvironment(new_env, bindings = TRUE) - id <- sample.int(.Machine$integer.max, size = length(code)) - methods::new( - "qenv", - env = new_env, code = code, warnings = rep("", length(code)), messages = rep("", length(code)), id = id - ) + new_qenv(env, format_expression(code)) } ) @@ -40,7 +34,14 @@ setMethod( "new_qenv", signature = c(env = "environment", code = "character"), function(env, code) { - new_qenv(env, code = parse(text = code, keep.source = FALSE)) + new_env <- rlang::env_clone(env, parent = parent.env(.GlobalEnv)) + lockEnvironment(new_env, bindings = TRUE) + if (length(code) > 0) code <- paste(code, collapse = "\n") + id <- sample.int(.Machine$integer.max, size = length(code)) + methods::new( + "qenv", + env = new_env, code = code, warnings = rep("", length(code)), messages = rep("", length(code)), id = id + ) } ) @@ -50,8 +51,7 @@ setMethod( "new_qenv", signature = c(env = "environment", code = "language"), function(env, code) { - code_expr <- as.expression(code) - new_qenv(env = env, code = code_expr) + new_qenv(env = env, code = format_expression(code)) } ) diff --git a/R/qenv-eval_code.R b/R/qenv-eval_code.R index 131e6300..5bf49ab3 100644 --- a/R/qenv-eval_code.R +++ b/R/qenv-eval_code.R @@ -21,23 +21,25 @@ setGeneric("eval_code", function(object, code) standardGeneric("eval_code")) #' @rdname eval_code #' @export -setMethod("eval_code", signature = c("qenv", "expression"), function(object, code) { - id <- sample.int(.Machine$integer.max, size = length(code)) +setMethod("eval_code", signature = c("qenv", "character"), function(object, code) { + id <- sample.int(.Machine$integer.max, size = 1) object@id <- c(object@id, id) object@env <- rlang::env_clone(object@env, parent = parent.env(.GlobalEnv)) + code <- paste(code, collapse = "\n") object@code <- c(object@code, code) current_warnings <- "" current_messages <- "" - for (code_line in code) { - # Using withCallingHandlers to capture ALL warnings and messages. - # Using tryCatch to capture the FIRST error and abort further evaluation. + parsed_code <- parse(text = code, keep.source = TRUE) + for (single_call in parsed_code) { + # Using withCallingHandlers to capture warnings and messages. + # Using tryCatch to capture the error and abort further evaluation. x <- withCallingHandlers( tryCatch( { - eval(code_line, envir = object@env) + eval(single_call, envir = object@env) NULL }, error = function(e) { @@ -45,7 +47,7 @@ setMethod("eval_code", signature = c("qenv", "expression"), function(object, cod message = sprintf( "%s \n when evaluating qenv code:\n%s", .ansi_strip(conditionMessage(e)), - paste(format_expression(code), collapse = "\n") + deparse1(single_call) ), class = c("qenv.error", "try-error", "simpleError"), trace = object@code @@ -61,13 +63,16 @@ setMethod("eval_code", signature = c("qenv", "expression"), function(object, cod invokeRestart("muffleMessage") } ) + if (!is.null(x)) { return(x) } - - object@warnings <- c(object@warnings, current_warnings) - object@messages <- c(object@messages, current_messages) } + + + object@warnings <- c(object@warnings, current_warnings) + object@messages <- c(object@messages, current_messages) + lockEnvironment(object@env, bindings = TRUE) object }) @@ -75,14 +80,13 @@ setMethod("eval_code", signature = c("qenv", "expression"), function(object, cod #' @rdname eval_code #' @export setMethod("eval_code", signature = c("qenv", "language"), function(object, code) { - code_char <- as.expression(code) - eval_code(object, code_char) + eval_code(object, code = format_expression(code)) }) #' @rdname eval_code #' @export -setMethod("eval_code", signature = c("qenv", "character"), function(object, code) { - eval_code(object, code = parse(text = code, keep.source = FALSE)) +setMethod("eval_code", signature = c("qenv", "expression"), function(object, code) { + eval_code(object, code = format_expression(code)) }) #' @rdname eval_code diff --git a/R/qenv-get_code.R b/R/qenv-get_code.R index 5ea6b43a..ce5dd439 100644 --- a/R/qenv-get_code.R +++ b/R/qenv-get_code.R @@ -26,9 +26,9 @@ setGeneric("get_code", function(object, deparse = TRUE) { setMethod("get_code", signature = "qenv", function(object, deparse = TRUE) { checkmate::assert_flag(deparse) if (deparse) { - format_expression(object@code) - } else { object@code + } else { + parse(text = object@code, keep.source = TRUE) } }) diff --git a/R/utils-code_dependency.R b/R/utils-code_dependency.R new file mode 100644 index 00000000..72c7d677 --- /dev/null +++ b/R/utils-code_dependency.R @@ -0,0 +1,360 @@ +#' Create Object Dependencies Structure Within Parsed Code +#' +#' @description This function constructs a dependency structure that identifies the relationships between objects in +#' parsed code. It helps you understand which objects are needed to recreate a specific object. +#' +#' @details This function assumes that object relationships are established using the `<-`, `=`, or `->` assignment +#' operators. It does not support other object creation methods like `assign` or `<<-`, nor non-standard-evaluation +#' methods. To specify relationships between side-effects and objects, you can use the comment tag +#' `# @linksto object_name` at the end of a line where the side-effect occurs. +#' +#' @param code An `expression` with `srcref` attribute or a `character` with the code. +#' @param object_names (`character(n)`) A vector containing the names of existing objects. +#' +#' @return A `list` with three components: +#' - `occurrence`: A named `list` where object names are the names of existing objects, and each element is a numeric +#' vector indicating the calls in which the object appears. +#' - `cooccurrence`: A `list` of the same length as the number of calls in `parsed_code` +#' (`parsed_code = parse(text = code)` for code input as `character` and `parsed_code = code` for expression input. +#' It contains `NULL` values if there is no co-occurrence between objects or a `character` vector indicating the +#' co-occurrence of objects in a specific `parsed_code` call element. If it's a character vector, the first element is +#' the name of the dependent object, and the rest are the influencing objects. +#' - `effects`: A named `list` where object names are the names of existing objects, and each element is a numeric +#' vector indicating which calls have an effect on that object. If there are no side-effects pointing at an object, +#' the element is `NULL`. +#' +#' +#' @keywords internal +#' +code_dependency <- function(code, object_names) { + checkmate::assert_multi_class(code, classes = c("character", "expression")) + checkmate::assert_character(object_names, null.ok = TRUE) + + if (is.expression(code)) { + if (!is.null(attr(code, "srcref"))) { + parsed_code <- code + } else { + stop("The 'expression' code input does not contain 'srcref' attribute.") + } + } + + if (is.character(code)) { + parsed_code <- parse(text = code, keep.source = TRUE) + } + + pd <- utils::getParseData(parsed_code) + + calls_pd <- lapply(pd[pd$parent == 0, "id"], get_children, pd = pd) + + occurrence <- lapply(sapply(object_names, detect_symbol, calls_pd = calls_pd, simplify = FALSE), which) + + cooccurrence <- lapply( + calls_pd, + function(x) { + sym_cond <- which(x$token %in% c("SYMBOL", "SYMBOL_FUNCTION_CALL") & x$text %in% object_names) + sym_form_cond <- which(x$token == "SYMBOL_FORMALS" & x$text %in% object_names) + sym_cond <- sym_cond[!x[sym_cond, "text"] %in% x[sym_form_cond, "text"]] + + object_ids <- x[sym_cond, "id"] + dollar_ids <- x[x$"token" %in% c("'$'", "'@'"), "id"] + after_dollar <- object_ids[(object_ids - 2) %in% dollar_ids] + sym_cond <- setdiff(sym_cond, which(x$id %in% after_dollar)) + + if (length(sym_cond) >= 2) { + ass_cond <- grep("ASSIGN", x$token) + text <- unique(x[sort(c(sym_cond, ass_cond)), "text"]) + + if (text[1] == "->") { + rev(text[-1]) + } else { + text[-1] + } + } + } + ) + + side_effects <- grep("@linksto", pd[pd$token == "COMMENT", "text"], value = TRUE) + check_effects <- + if (length(side_effects) > 0) { + affected <- + unlist(strsplit(sub("\\s*#\\s*@linksto\\s+", "", side_effects), "\\s+")) + + union(object_names, affected) + } else { + object_names + } + + effects <- sapply( + check_effects, + function(x) { + maxid <- suppressWarnings(max(occurrence[[x]])) + return_code_for_effects( + x, + calls_pd = calls_pd, + occur = suppressWarnings(lapply(occurrence, function(x) setdiff(x, maxid:max(maxid, max(x))))), + cooccur = cooccurrence, + eff = NULL + ) + }, + simplify = FALSE + ) + + list( + occurrence = occurrence, + cooccurrence = cooccurrence, + effects = effects + ) +} + +#' @title Get child calls within `getParseData()` object +#' @param pd (`data.frame`) A result of `utils::getParseData()`. +#' @param parent Object parent id in `utils::getParseData()`. +#' @return Row `bounded` `utils::getParseData()` of all elements of a call pointing to a `parent` id. +#' @keywords internal +get_children <- function(pd, parent) { + idx_children <- abs(pd$parent) == parent + children <- pd[idx_children, c("token", "text", "id")] + if (nrow(children) == 0) { + return(NULL) + } + + if (parent > 0) { + do.call(rbind, c(list(children), lapply(children$id, get_children, pd = pd))) + } +} + +#' @title Detects `"SYMBOL"` tokens for row `bounded` `getParseData()` structure +#' @param object `character` containing the name of the object +#' @param calls_pd A `list` of `data.frame`s, which is a result of `get_children(utils::getParseData(), parent = 0)` +#' applied on `parse(text = code, keep.source = TRUE)` at `code_dependency(code)`. +#' @return A `logical` vector pointing in which elements of `pd` the `SYMBOL` token row has `object` in text column +#' @keywords internal +detect_symbol <- function(object, calls_pd) { + vapply( + calls_pd, + function(call) { + is_symbol <- + any(call[call$token %in% c("SYMBOL", "SYMBOL_FUNCTION_CALL"), "text"] == object) + + is_formal <- used_in_function(call, object) + + object_ids <- call[call$text == object, "id"] + dollar_ids <- call[call$"token" %in% c("'$'", "'@'"), "id"] + after_dollar <- object_ids[(object_ids - 2) %in% dollar_ids] + object_ids <- setdiff(object_ids, after_dollar) + + is_symbol & !is_formal & length(object_ids) > 0 + }, + logical(1) + ) +} + +#' @title Whether an object is used inside a function within a call +#' @param call An element of `calls_pd` list used in `detect_symbol`. +#' @param object A character with object name. +#' @return A `logical(1)`. +#' @keywords internal +used_in_function <- function(call, object) { + if (any(call[call$token == "SYMBOL_FORMALS", "text"] == object) && any(call$token == "FUNCTION")) { + object_sf_ids <- call[call$text == object & call$token == "SYMBOL", "id"] + function_start_id <- call[call$token == "FUNCTION", "id"] + all(object_sf_ids > function_start_id) + } else { + FALSE + } +} + +#' Return the lines of code needed to reproduce the object. +#' @return `numeric` vector indicating which lines of `parsed_code` calls are required to build the `object` +#' +#' @param object `character` with object name +#' @param occur result of `code_dependency()$occurrence` +#' @param cooccur result of `code_dependency()$cooccurrence` +#' @param eff result of `code_dependency()$effects` +#' @param parent `NULL` or `numeric` vector - in a recursive call, it is possible needed to drop parent object +#' indicator to omit dependency cycles +#' +#' @return A `numeric` vector with number of lines of input `pd` to be returned. +#' +#' @keywords internal +return_code <- function(object, occur, cooccur, eff, parent = NULL) { + if (all(unlist(lapply(occur, length)) == 0)) { + return(NULL) + } + + influences <- vapply(cooccur, match, integer(1L), x = object) + where_influences <- which(influences > 1L) + object_influencers <- which(influences == 1L) + + object_influencers <- setdiff(object_influencers, parent) + + lines <- setdiff(occur[[object]], where_influences) + + if (length(object_influencers) == 0) { + return(sort(unique(lines))) + } else { + for (idx in object_influencers) { + influencer_names <- cooccur[[idx]][-1] + + influencer_lines <- + unlist( + lapply( + influencer_names, + return_code, + occur = suppressWarnings(lapply(occur, function(x) setdiff(x, idx:max(idx, max(x))))), + cooccur = cooccur[1:idx], + parent = where_influences, + eff = eff + ) + ) + + influencer_effects_lines <- unlist(eff[influencer_names]) + lines <- c(lines, influencer_lines, influencer_effects_lines) + } + sort(unique(lines)) + } +} + +#' Return the lines of code needed to reproduce the side-effects having an impact on the object. +#' @return `numeric` vector indicating which lines of `parsed_code` calls are required to build the side-effects having +#' and impact on the `object` +#' +#' @param object `character` with object name +#' @param calls_pd A `list` of `data.frame`s, which is a result of `get_children(utils::getParseData(), parent = 0)` +#' applied on `parse(text = code, keep.source = TRUE)` at `code_dependency(code)`. +#' @param occur result of `code_dependency()$occurrence` +#' @param cooccur result of `code_dependency()$cooccurrence` +#' +#' @return A `numeric` vector with number of lines of input `pd` to be returned for effects. +#' +#' @keywords internal +return_code_for_effects <- function(object, calls_pd, occur, cooccur, eff) { + symbol_effects_names <- + unlist( + lapply( + calls_pd, + function(x) { + com_cond <- + x$token == "COMMENT" & grepl("@linksto", x$text) & grepl(paste0("[\\s]*", object, "[\\s$]*"), x$text) + + # Make sure comment id is not the highest id in the item. + # For calls like 'options(prompt = ">") # @linksto ADLB', + # 'options(prompt = ">")' is put in a one item + # and '# @linksto ADLB' is the first element of the next item. + # This is tackled in B. + + + if (!com_cond[1] & sum(com_cond) > 0) { + # A. + x[x$token == "SYMBOL", "text"] + } else if (com_cond[1] & sum(com_cond[-1]) > 0) { + # B. + x <- x[-1, ] + x[x$token == "SYMBOL", "text"] + } + } + ) + ) + + commented_calls <- vapply( + calls_pd, + function(x) any(x$token == "COMMENT" & grepl("@linksto", x$text)), + logical(1) + ) + + symbol_effects_lines <- + unlist( + lapply( + symbol_effects_names, + function(x) { + code <- return_code(x, occur = occur, cooccur = cooccur, eff = eff) + if (is.null(code)) { + # Below is just used for comments with @linksto. + intersect(which(detect_symbol(x, calls_pd)), which(commented_calls)) + } else { + code + } + } + ) + ) + + # When commet_id is the highest id in the item - take previous item. + side_effects_names <- + unlist( + lapply( + calls_pd, + function(x) { + com_cond <- + x$token == "COMMENT" & grepl("@linksto", x$text) & grepl(paste0("[\\s]*", object, "[\\s$]*"), x$text) + + # Work out the situation when comment id is the highest id in the item. + # For calls like 'options(prompt = ">") # @linksto ADLB', + # 'options(prompt = ">")' is put in a one item + # and '# @linksto ADLB' is the first element of the next item. + + com_cond[1] + } + ) + ) + + side_effects_lines <- which(side_effects_names) - 1 + + sort(unique(c(symbol_effects_lines, side_effects_lines))) +} + +#' Return the lines of code (with side-effects) needed to reproduce the object +#' @return `character` vector of elements of `code` calls that were required to build the side-effects and +#' influencing objects having and impact on the `object` +#' +#' @param code An `expression` with `srcref` attribute or a `character` with the code. +#' @param names A `character(n)` with object names. +#' @keywords internal +get_code_dependency <- function(code, names) { + checkmate::assert_multi_class(code, classes = c("character", "expression")) + checkmate::assert_character(names) + + if (is.expression(code)) { + if (!is.null(attr(code, "srcref"))) { + parsed_code <- code + } else { + stop("The 'expression' code input does not contain 'srcref' attribute.") + } + } + + if (is.character(code)) { + parsed_code <- parse(text = code, keep.source = TRUE) + } + + pd <- utils::getParseData(parsed_code) + + symbols <- unique(pd[pd$token == "SYMBOL", "text"]) + + if (!all(names %in% symbols)) { + warning( + "Objects not found in 'qenv' environment: ", + toString(setdiff(names, symbols)) + ) + } + + code_dependency <- code_dependency(parsed_code, symbols) + + lines <- + sapply(names, function(name) { + object_lines <- + return_code( + name, + occur = code_dependency$occurrence, + cooccur = code_dependency$cooccurrence, + eff = code_dependency$effects + ) + + effects_lines <- code_dependency$effects[[name]] + c(object_lines, effects_lines) + }, + simplify = FALSE + ) + + object_lines_unique <- sort(unique(unlist(lines))) + + as.character(parsed_code)[object_lines_unique] +} diff --git a/R/utils.R b/R/utils.R index a1c155a7..ae090be5 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,41 +1,3 @@ -#' Removes leading and trailing curly brackets from character -#' string and removes indentation of remaining contents -#' -#' @description `r lifecycle::badge("stable")` -#' @param x (`character`)\cr -#' -#' @return character string without curly braces -#' @keywords internal -remove_enclosing_curly_braces <- function(x) { - checkmate::assert_character(x) - if (length(x) == 0) { - return(x) - } - - open_bracket_and_spaces <- "^[[:blank:]]*\\{[[:blank:]]*$" - close_bracket_and_spaces <- "^[[:blank:]]*\\}[[:blank:]]*$" - blank_line <- "^[[:blank:]]*$" - four_spaces_at_start_of_line <- "^[[:blank:]]{4}" - - split_text <- unlist(strsplit(x, "\n", fixed = TRUE)) - - # if text begins with "{ \n" and ends with "\n} " - if (grepl(open_bracket_and_spaces, utils::head(split_text, 1)) && - grepl(close_bracket_and_spaces, utils::tail(split_text, 1))) { - # remove the first and last line - split_text <- split_text[-c(1, length(split_text))] - - # if any line is not blank then indent - if (!all(grepl(blank_line, split_text))) { - return(gsub(four_spaces_at_start_of_line, "", split_text)) - } else { - return(split_text) - } - } else { - return(split_text) - } -} - #' Suppresses plot display in the IDE by opening a PDF graphics device #' #' This function opens a PDF graphics device using \code{\link[grDevices]{pdf}} to suppress @@ -62,7 +24,30 @@ dev_suppress <- function(x) { force(x) } -# converts vector of expressions to character format_expression <- function(code) { - as.character(styler::style_text(unlist(lapply(as.character(code), remove_enclosing_curly_braces)))) + code <- lang2calls(code) + paste(code, collapse = "\n") +} + + +# convert language object or lists of language objects to list of simple calls +# @param x `language` object or a list of thereof +# @return +# Given a `call`, an `expression`, a list of `call`s or a list of `expression`s, +# returns a list of `calls`. +# Symbols and atomic vectors (which may get mixed up in a list) are returned wrapped in list. +#' @keywords internal +lang2calls <- function(x) { + if (is.atomic(x) || is.symbol(x)) { + return(list(x)) + } + if (is.call(x)) { + if (identical(as.list(x)[[1L]], as.symbol("{"))) { + as.list(x)[-1L] + } else { + list(x) + } + } else { + unlist(lapply(x, lang2calls), recursive = FALSE) + } } diff --git a/man/code_dependency.Rd b/man/code_dependency.Rd new file mode 100644 index 00000000..87d815a2 --- /dev/null +++ b/man/code_dependency.Rd @@ -0,0 +1,39 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-code_dependency.R +\name{code_dependency} +\alias{code_dependency} +\title{Create Object Dependencies Structure Within Parsed Code} +\usage{ +code_dependency(code, object_names) +} +\arguments{ +\item{code}{An \code{expression} with \code{srcref} attribute or a \code{character} with the code.} + +\item{object_names}{(\code{character(n)}) A vector containing the names of existing objects.} +} +\value{ +A \code{list} with three components: +\itemize{ +\item \code{occurrence}: A named \code{list} where object names are the names of existing objects, and each element is a numeric +vector indicating the calls in which the object appears. +\item \code{cooccurrence}: A \code{list} of the same length as the number of calls in \code{parsed_code} +(\code{parsed_code = parse(text = code)} for code input as \code{character} and \code{parsed_code = code} for expression input. +It contains \code{NULL} values if there is no co-occurrence between objects or a \code{character} vector indicating the +co-occurrence of objects in a specific \code{parsed_code} call element. If it's a character vector, the first element is +the name of the dependent object, and the rest are the influencing objects. +\item \code{effects}: A named \code{list} where object names are the names of existing objects, and each element is a numeric +vector indicating which calls have an effect on that object. If there are no side-effects pointing at an object, +the element is \code{NULL}. +} +} +\description{ +This function constructs a dependency structure that identifies the relationships between objects in +parsed code. It helps you understand which objects are needed to recreate a specific object. +} +\details{ +This function assumes that object relationships are established using the \verb{<-}, \code{=}, or \verb{->} assignment +operators. It does not support other object creation methods like \code{assign} or \verb{<<-}, nor non-standard-evaluation +methods. To specify relationships between side-effects and objects, you can use the comment tag +\verb{# @linksto object_name} at the end of a line where the side-effect occurs. +} +\keyword{internal} diff --git a/man/detect_symbol.Rd b/man/detect_symbol.Rd new file mode 100644 index 00000000..28bacfde --- /dev/null +++ b/man/detect_symbol.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-code_dependency.R +\name{detect_symbol} +\alias{detect_symbol} +\title{Detects \code{"SYMBOL"} tokens for row \code{bounded} \code{getParseData()} structure} +\usage{ +detect_symbol(object, calls_pd) +} +\arguments{ +\item{object}{\code{character} containing the name of the object} + +\item{calls_pd}{A \code{list} of \code{data.frame}s, which is a result of \code{get_children(utils::getParseData(), parent = 0)} +applied on \code{parse(text = code, keep.source = TRUE)} at \code{code_dependency(code)}.} +} +\value{ +A \code{logical} vector pointing in which elements of \code{pd} the \code{SYMBOL} token row has \code{object} in text column +} +\description{ +Detects \code{"SYMBOL"} tokens for row \code{bounded} \code{getParseData()} structure +} +\keyword{internal} diff --git a/man/eval_code.Rd b/man/eval_code.Rd index ec14d1b9..3a1e1c38 100644 --- a/man/eval_code.Rd +++ b/man/eval_code.Rd @@ -2,19 +2,19 @@ % Please edit documentation in R/qenv-eval_code.R \name{eval_code} \alias{eval_code} -\alias{eval_code,qenv,expression-method} -\alias{eval_code,qenv,language-method} \alias{eval_code,qenv,character-method} +\alias{eval_code,qenv,language-method} +\alias{eval_code,qenv,expression-method} \alias{eval_code,qenv.error,ANY-method} \title{Evaluate the code in the \code{qenv} environment} \usage{ eval_code(object, code) -\S4method{eval_code}{qenv,expression}(object, code) +\S4method{eval_code}{qenv,character}(object, code) \S4method{eval_code}{qenv,language}(object, code) -\S4method{eval_code}{qenv,character}(object, code) +\S4method{eval_code}{qenv,expression}(object, code) \S4method{eval_code}{qenv.error,ANY}(object, code) } diff --git a/man/get_children.Rd b/man/get_children.Rd new file mode 100644 index 00000000..73014b64 --- /dev/null +++ b/man/get_children.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-code_dependency.R +\name{get_children} +\alias{get_children} +\title{Get child calls within \code{getParseData()} object} +\usage{ +get_children(pd, parent) +} +\arguments{ +\item{pd}{(\code{data.frame}) A result of \code{utils::getParseData()}.} + +\item{parent}{Object parent id in \code{utils::getParseData()}.} +} +\value{ +Row \code{bounded} \code{utils::getParseData()} of all elements of a call pointing to a \code{parent} id. +} +\description{ +Get child calls within \code{getParseData()} object +} +\keyword{internal} diff --git a/man/get_code_dependency.Rd b/man/get_code_dependency.Rd new file mode 100644 index 00000000..6f82f6b6 --- /dev/null +++ b/man/get_code_dependency.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-code_dependency.R +\name{get_code_dependency} +\alias{get_code_dependency} +\title{Return the lines of code (with side-effects) needed to reproduce the object} +\usage{ +get_code_dependency(code, names) +} +\arguments{ +\item{code}{An \code{expression} with \code{srcref} attribute or a \code{character} with the code.} + +\item{names}{A \code{character(n)} with object names.} +} +\value{ +\code{character} vector of elements of \code{code} calls that were required to build the side-effects and +influencing objects having and impact on the \code{object} +} +\description{ +Return the lines of code (with side-effects) needed to reproduce the object +} +\keyword{internal} diff --git a/man/new_qenv.Rd b/man/new_qenv.Rd index a91672e9..d85e0553 100644 --- a/man/new_qenv.Rd +++ b/man/new_qenv.Rd @@ -8,15 +8,15 @@ \alias{new_qenv,missing,missing-method} \title{Initialize \code{qenv} object} \usage{ -new_qenv(env = new.env(parent = parent.env(.GlobalEnv)), code = expression()) +new_qenv(env = new.env(parent = parent.env(.GlobalEnv)), code = character()) -\S4method{new_qenv}{environment,expression}(env = new.env(parent = parent.env(.GlobalEnv)), code = expression()) +\S4method{new_qenv}{environment,expression}(env = new.env(parent = parent.env(.GlobalEnv)), code = character()) -\S4method{new_qenv}{environment,character}(env = new.env(parent = parent.env(.GlobalEnv)), code = expression()) +\S4method{new_qenv}{environment,character}(env = new.env(parent = parent.env(.GlobalEnv)), code = character()) -\S4method{new_qenv}{environment,language}(env = new.env(parent = parent.env(.GlobalEnv)), code = expression()) +\S4method{new_qenv}{environment,language}(env = new.env(parent = parent.env(.GlobalEnv)), code = character()) -\S4method{new_qenv}{missing,missing}(env = new.env(parent = parent.env(.GlobalEnv)), code = expression()) +\S4method{new_qenv}{missing,missing}(env = new.env(parent = parent.env(.GlobalEnv)), code = character()) } \arguments{ \item{env}{(\code{environment}) Environment being a result of the \code{code} evaluation.} @@ -33,7 +33,7 @@ can create an empty \code{qenv} and evaluate the expressions in this object usin } \examples{ new_qenv(env = list2env(list(a = 1)), code = quote(a <- 1)) -new_qenv(env = list2env(list(a = 1)), code = parse(text = "a <- 1")) +new_qenv(env = list2env(list(a = 1)), code = parse(text = "a <- 1", keep.source = TRUE)) new_qenv(env = list2env(list(a = 1)), code = "a <- 1") } diff --git a/man/qenv-class.Rd b/man/qenv-class.Rd index 476fb83d..163ef462 100644 --- a/man/qenv-class.Rd +++ b/man/qenv-class.Rd @@ -10,7 +10,7 @@ Reproducible class with environment and code. \section{Slots}{ \describe{ -\item{\code{code}}{(\code{expression}) to reproduce the environment} +\item{\code{code}}{(\code{character}) representing code necessary to reproduce the environment} \item{\code{env}}{(\code{environment}) environment which content was generated by the evaluation of the \code{code} slot.} diff --git a/man/remove_enclosing_curly_braces.Rd b/man/remove_enclosing_curly_braces.Rd deleted file mode 100644 index 359fb6b5..00000000 --- a/man/remove_enclosing_curly_braces.Rd +++ /dev/null @@ -1,19 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R -\name{remove_enclosing_curly_braces} -\alias{remove_enclosing_curly_braces} -\title{Removes leading and trailing curly brackets from character -string and removes indentation of remaining contents} -\usage{ -remove_enclosing_curly_braces(x) -} -\arguments{ -\item{x}{(\code{character})\cr} -} -\value{ -character string without curly braces -} -\description{ -\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#stable}{\figure{lifecycle-stable.svg}{options: alt='[Stable]'}}}{\strong{[Stable]}} -} -\keyword{internal} diff --git a/man/return_code.Rd b/man/return_code.Rd new file mode 100644 index 00000000..614676c4 --- /dev/null +++ b/man/return_code.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-code_dependency.R +\name{return_code} +\alias{return_code} +\title{Return the lines of code needed to reproduce the object.} +\usage{ +return_code(object, occur, cooccur, eff, parent = NULL) +} +\arguments{ +\item{object}{\code{character} with object name} + +\item{occur}{result of \code{code_dependency()$occurrence}} + +\item{cooccur}{result of \code{code_dependency()$cooccurrence}} + +\item{eff}{result of \code{code_dependency()$effects}} + +\item{parent}{\code{NULL} or \code{numeric} vector - in a recursive call, it is possible needed to drop parent object +indicator to omit dependency cycles} +} +\value{ +\code{numeric} vector indicating which lines of \code{parsed_code} calls are required to build the \code{object} + +A \code{numeric} vector with number of lines of input \code{pd} to be returned. +} +\description{ +Return the lines of code needed to reproduce the object. +} +\keyword{internal} diff --git a/man/return_code_for_effects.Rd b/man/return_code_for_effects.Rd new file mode 100644 index 00000000..f6285932 --- /dev/null +++ b/man/return_code_for_effects.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-code_dependency.R +\name{return_code_for_effects} +\alias{return_code_for_effects} +\title{Return the lines of code needed to reproduce the side-effects having an impact on the object.} +\usage{ +return_code_for_effects(object, calls_pd, occur, cooccur, eff) +} +\arguments{ +\item{object}{\code{character} with object name} + +\item{calls_pd}{A \code{list} of \code{data.frame}s, which is a result of \code{get_children(utils::getParseData(), parent = 0)} +applied on \code{parse(text = code, keep.source = TRUE)} at \code{code_dependency(code)}.} + +\item{occur}{result of \code{code_dependency()$occurrence}} + +\item{cooccur}{result of \code{code_dependency()$cooccurrence}} +} +\value{ +\code{numeric} vector indicating which lines of \code{parsed_code} calls are required to build the side-effects having +and impact on the \code{object} + +A \code{numeric} vector with number of lines of input \code{pd} to be returned for effects. +} +\description{ +Return the lines of code needed to reproduce the side-effects having an impact on the object. +} +\keyword{internal} diff --git a/man/used_in_function.Rd b/man/used_in_function.Rd new file mode 100644 index 00000000..cc791d8a --- /dev/null +++ b/man/used_in_function.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-code_dependency.R +\name{used_in_function} +\alias{used_in_function} +\title{Whether an object is used inside a function within a call} +\usage{ +used_in_function(call, object) +} +\arguments{ +\item{call}{An element of \code{calls_pd} list used in \code{detect_symbol}.} + +\item{object}{A character with object name.} +} +\value{ +A \code{logical(1)}. +} +\description{ +Whether an object is used inside a function within a call +} +\keyword{internal} diff --git a/tests/testthat/test-code_dependency.R b/tests/testthat/test-code_dependency.R new file mode 100644 index 00000000..5a4baf14 --- /dev/null +++ b/tests/testthat/test-code_dependency.R @@ -0,0 +1,375 @@ +testthat::test_that("get_code_dependency extract code of a binding from a simple code put in a character", { + q <- c( + "a <- 1", + "b <- 2" + ) + testthat::expect_identical( + get_code_dependency(q, names = "a"), + "a <- 1" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + "b <- 2" + ) +}) + +testthat::test_that("get_code_dependency warns if binding doesn't exist in a code", { + q <- c( + "a <- 1", + "b <- 2" + ) + testthat::expect_warning( + get_code_dependency(q, names = "c") + ) +}) + + +testthat::test_that( + "get_code_dependency extracts code of a parent binding but only those evaluated before coocurence", + { + q <- c( + "a <- 1", + "b <- a", + "a <- 2" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("a <- 1", "b <- a") + ) + } +) + +testthat::test_that("get_code_dependency extracts code of a parent binding if used as an arg in fun call", { + q <- c( + "a <- 1", + "b <- identity(x = a)", + "a <- 2" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("a <- 1", "b <- identity(x = a)") + ) +}) + +testthat::test_that("get_code_dependency is possible to output the code for multiple objects", { + q <- c( + "a <- 1", + "b <- 2", + "c <- 3" + ) + testthat::expect_identical( + get_code_dependency(q, names = c("a", "b")), + c("a <- 1", "b <- 2") + ) +}) + +testthat::test_that("get_code_dependency can't extract the code when no assign operator", { + q <- c( + "a <- 1", + "assign('b', 5)", + "b <- b + 2" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + "b <- b + 2" + ) +}) + +testthat::test_that("@linksto tag indicate affected object if object is assigned anywhere in a code", { + q <- c( + "a <- 1", + "assign('b', 5) # @linksto b", + "b <- b + 2" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("assign(\"b\", 5)", "b <- b + 2") + ) +}) + + +testthat::test_that( + "get_code_dependency can extract the code when function creates an object which is used only on rhs", + { + q <- c( + "data(iris)", + "iris2 <- head(iris)" + ) + testthat::expect_identical( + get_code_dependency(q, names = "iris2"), + c("data(iris)", "iris2 <- head(iris)") + ) + } +) + +testthat::test_that("get_code_dependency can extract the code when using <<-", { + q <- c( + "a <- 1", + "b <- a", + "b <<- b + 2" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("a <- 1", "b <- a", "b <<- b + 2") + ) +}) + +testthat::test_that("get_code_dependency detects every assign calls even if not evaluated", { + q <- c( + "a <- 1", + "b <- 2", + "eval(expression({b <- b + 2}))" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("b <- 2", "eval(expression({\n b <- b + 2\n}))") + ) +}) + + +# @linksto --------------------------------------------------------------------------------------------------------- + + +testthat::test_that("@linksto cause to return this line for affected binding", { + q <- " + a <- 1 # @linksto b + b <- 2 + " + + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("a <- 1", "b <- 2") + ) +}) + +testthat::test_that( + "@linksto returns this line for affected binding + even if object is not specificed/created in the same eval_code", + { + q <- c( + "a <- 1 # @linksto b", + "b <- 2" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("a <- 1", "b <- 2") + ) + } +) + +testthat::test_that( + "@linksto returns this line for affected binding + if object is not specificed in the same element of code", + { + q <- c( + "a <- 1 ", + "b <- 2 # @linksto a" + ) + testthat::expect_identical( + get_code_dependency(q, names = "a"), + c("a <- 1", "b <- 2") + ) + } +) + +testthat::test_that( + "lines affecting parent evaluated after co-occurrence are not included in get_code_dependency output", + { + q <- c( + "a <- 1", + "b <- a", + "a <- 3" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("a <- 1", "b <- a") + ) + } +) + +testthat::test_that( + "lines affecting parent evaluated after co-occurrence are not included in get_code_dependency output + when using @linksto", + { + q <- c( + "a <- 1 ", + "b <- 2 # @linksto a", + "a <- a + 1", + "b <- b + 1" + ) + testthat::expect_identical( + get_code_dependency(q, names = "a"), + c("a <- 1", "b <- 2", "a <- a + 1") + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("b <- 2", "b <- b + 1") + ) + } +) + +testthat::test_that( + "@linksto gets extracted if it's a side-effect on a dependent object", + { + q <- " + iris[1:5, ] -> iris2 + iris_head <- head(iris) # @linksto iris2 + classes <- lapply(iris2, class) + " + testthat::expect_identical( + get_code_dependency(q, names = "classes"), + c("iris2 <- iris[1:5, ]", "iris_head <- head(iris)", "classes <- lapply(iris2, class)") + ) + } +) + +testthat::test_that( + "@linksto gets extracted if it's a side-effect on a dependent object of a dependent object", + { + q <- " + iris[1:5, ] -> iris2 + iris_head <- head(iris) # @linksto iris3 + iris3 <- iris_head[1, ] # @linksto iris2 + classes <- lapply(iris2, class) + " + testthat::expect_identical( + get_code_dependency(q, names = "classes"), + c("iris2 <- iris[1:5, ]", "iris_head <- head(iris)", "iris3 <- iris_head[1, ]", "classes <- lapply(iris2, class)") + ) + } +) + +# functions ------------------------------------------------------------------------------------------------------- + +testthat::test_that("get_code_dependency ignores occurrence in function definition", { + q <- c( + "b <- 2", + "foo <- function(b) { b <- b + 2 }" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + "b <- 2" + ) + testthat::expect_identical( + get_code_dependency(q, names = "foo"), + "foo <- function(b) {\n b <- b + 2\n}" + ) +}) + +testthat::test_that("get_code_dependency ignores occurrence in function definition without { curly brackets", { + q <- c( + "b <- 2", + "foo <- function(b) b <- b + 2 " + ) + testthat::expect_identical( + get_code_dependency(q, names = "foo"), + "foo <- function(b) b <- b + 2" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + "b <- 2" + ) +}) + +testthat::test_that("get_code_dependency ignores effect of the object which occurs in a function definition", { + q <- c( + "b <- 2", + "foo <- function(b) { b <- b + 2 }" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("b <- 2") + ) +}) + +testthat::test_that("get_code_dependency detects occurrence of the function object", { + q <- c( + "a <- 1", + "b <- 2", + "foo <- function(b) { b <- b + 2 }", + "b <- foo(a)" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("a <- 1", "b <- 2", "foo <- function(b) {\n b <- b + 2\n}", "b <- foo(a)") + ) +}) + +testthat::test_that( + "Can't detect occurrence of function definition when a formal is named the same as a function", + { + q <- c( + "x <- 1", + "foo <- function(foo = 1) 'text'", + "a <- foo(x)" + ) + testthat::expect_identical( + get_code_dependency(q, names = "a"), + c("x <- 1", "foo <- function(foo = 1) \"text\"", "a <- foo(x)") + ) + } +) + +# $ --------------------------------------------------------------------------------------------------------------- + +testthat::test_that("get_code_dependency understands $ usage and do not treat rhs of $ as objects (only lhs)", { + q <- c( + "x <- data.frame(a = 1:3)", + "a <- data.frame(y = 1:3)", + "a$x <- a$y", + "a$x <- a$x + 2", + "a$x <- x$a" + ) + testthat::expect_identical( + get_code_dependency(q, names = "x"), + c("x <- data.frame(a = 1:3)") + ) + testthat::expect_identical( + get_code_dependency(q, names = "a"), + c("x <- data.frame(a = 1:3)", "a <- data.frame(y = 1:3)", "a$x <- a$y", "a$x <- a$x + 2", "a$x <- x$a") + ) +}) + +testthat::test_that("get_code_dependency detects cooccurrence properly even if all objects are on rhs", { + q <- c( + "a <- 1", + "b <- list(c = 2)", + "b[[a]] <- 3" + ) + testthat::expect_identical( + get_code_dependency(q, names = "b"), + c("a <- 1", "b <- list(c = 2)", "b[[a]] <- 3") + ) +}) + + +# @ --------------------------------------------------------------------------------------------------------------- + +testthat::test_that("get_code_dependency understands @ usage and do not treat rhs of @ as objects (only lhs)", { + q <- c( + "setClass('aclass', slots = c(a = 'numeric', x = 'numeric', y = 'numeric')) # @linksto a x", + "x <- new('aclass', a = 1:3, x = 1:3, y = 1:3)", + "a <- new('aclass', a = 1:3, x = 1:3, y = 1:3)", + "a@x <- a@y", + "a@x <- a@x + 2", + "a@x <- x@a" + ) + testthat::expect_identical( + get_code_dependency(q, names = "x"), + c( + 'setClass("aclass", slots = c(a = "numeric", x = "numeric", y = "numeric"))', + 'x <- new("aclass", a = 1:3, x = 1:3, y = 1:3)' + ) + ) + testthat::expect_identical( + get_code_dependency(q, names = "a"), + c( + 'setClass("aclass", slots = c(a = "numeric", x = "numeric", y = "numeric"))', + 'x <- new("aclass", a = 1:3, x = 1:3, y = 1:3)', + 'a <- new("aclass", a = 1:3, x = 1:3, y = 1:3)', + "a@x <- a@y", + "a@x <- a@x + 2", + "a@x <- x@a" + ) + ) +}) diff --git a/tests/testthat/test-qenv-within.R b/tests/testthat/test-qenv-within.R index 05ab6c1f..cc9e278f 100644 --- a/tests/testthat/test-qenv-within.R +++ b/tests/testthat/test-qenv-within.R @@ -48,18 +48,18 @@ testthat::test_that("styling of input code does not impact evaluation results", all_code <- get_code(q) testthat::expect_identical( all_code, - rep(c("1 + 1", "2 + 2"), 4L) + rep("1 + 1\n2 + 2", 4L) ) }) # return value ---- -testthat::test_that("within.qenv renturns a deep copy of `data`", { +testthat::test_that("within.qenv renturns a `qenv` where `@env` is a deep copy of that in `data`", { q <- new_qenv() q <- within(new_qenv(), i <- iris) qq <- within(q, {}) - testthat::expect_equal(q, qq) - testthat::expect_false(identical(q, qq)) + testthat::expect_equal(q@env, qq@env) + testthat::expect_false(identical(q@env, qq@env)) }) testthat::test_that("within.qenv renturns qenv.error even if evaluation raises error", { diff --git a/tests/testthat/test-qenv_concat.R b/tests/testthat/test-qenv_concat.R index d9552336..4751d9fe 100644 --- a/tests/testthat/test-qenv_concat.R +++ b/tests/testthat/test-qenv_concat.R @@ -9,7 +9,7 @@ testthat::test_that("Concatenate two identical qenvs outputs", { testthat::expect_equal(q@env, env) testthat::expect_identical( q@code, - as.expression(list(quote(iris1 <- iris), quote(iris1 <- iris))) + c("iris1 <- iris", "iris1 <- iris") ) }) @@ -22,7 +22,7 @@ testthat::test_that("Concatenate two independent qenvs results in object having testthat::expect_equal(q@env, list2env(list(iris1 = iris, mtcars1 = mtcars))) testthat::expect_identical( q@code, - as.expression(c(quote(iris1 <- iris), quote(mtcars1 <- mtcars))) + c("iris1 <- iris", "mtcars1 <- mtcars") ) testthat::expect_identical(q@id, c(q1@id, q2@id)) }) diff --git a/tests/testthat/test-qenv_constructor.R b/tests/testthat/test-qenv_constructor.R index 6dd0ffe1..e8583bd2 100644 --- a/tests/testthat/test-qenv_constructor.R +++ b/tests/testthat/test-qenv_constructor.R @@ -2,7 +2,7 @@ testthat::test_that("constructor returns qenv if nothing is specified", { q <- new_qenv() testthat::expect_s4_class(q, "qenv") testthat::expect_identical(ls(q@env), character(0)) - testthat::expect_identical(q@code, expression()) + testthat::expect_identical(q@code, character(0)) testthat::expect_identical(q@id, integer(0)) testthat::expect_identical(q@warnings, character(0)) testthat::expect_identical(q@messages, character(0)) @@ -34,7 +34,7 @@ testthat::test_that("new_qenv works with code being character", { env$iris1 <- iris q <- new_qenv("iris1 <- iris", env = env) testthat::expect_equal(q@env, env) - testthat::expect_identical(q@code, as.expression(quote(iris1 <- iris))) + testthat::expect_identical(q@code, "iris1 <- iris") testthat::expect_true(checkmate::test_int(q@id)) }) @@ -43,7 +43,7 @@ testthat::test_that("new_qenv works with code being expression", { env$iris1 <- iris q <- new_qenv(as.expression(quote(iris1 <- iris)), env = env) testthat::expect_equal(q@env, env) - testthat::expect_identical(q@code, as.expression(quote(iris1 <- iris))) + testthat::expect_identical(q@code, "iris1 <- iris") testthat::expect_true(checkmate::test_int(q@id)) }) @@ -52,7 +52,7 @@ testthat::test_that("new_qenv works with code being quoted expression", { env$iris1 <- iris q <- new_qenv(quote(iris1 <- iris), env = env) testthat::expect_equal(q@env, env) - testthat::expect_identical(q@code, as.expression(quote(iris1 <- iris))) + testthat::expect_identical(q@code, "iris1 <- iris") testthat::expect_true(checkmate::test_int(q@id)) }) @@ -66,7 +66,7 @@ testthat::test_that("new_qenv works with code being length > 1", { ) testthat::expect_identical( q@code, - as.expression(c(quote(iris1 <- iris), quote(iris1$new <- 1L))) + "iris1 <- iris\niris1$new <- 1" ) testthat::expect_equal(q@env, env) }) diff --git a/tests/testthat/test-qenv_eval_code.R b/tests/testthat/test-qenv_eval_code.R index 9736b23a..853ab679 100644 --- a/tests/testthat/test-qenv_eval_code.R +++ b/tests/testthat/test-qenv_eval_code.R @@ -48,21 +48,21 @@ testthat::test_that("library have to be called separately before using function testthat::test_that("eval_code works with character", { q1 <- eval_code(new_qenv(), "a <- 1") - testthat::expect_identical(q1@code, as.expression(quote(a <- 1))) + testthat::expect_identical(q1@code, "a <- 1") testthat::expect_equal(q1@env, list2env(list(a = 1))) }) testthat::test_that("eval_code works with expression", { q1 <- eval_code(new_qenv(), as.expression(quote(a <- 1))) - testthat::expect_identical(q1@code, as.expression(quote(a <- 1))) + testthat::expect_identical(q1@code, "a <- 1") testthat::expect_equal(q1@env, list2env(list(a = 1))) }) testthat::test_that("eval_code works with quoted", { q1 <- eval_code(new_qenv(), quote(a <- 1)) - testthat::expect_identical(q1@code, as.expression(quote(a <- 1))) + testthat::expect_identical(q1@code, "a <- 1") testthat::expect_equal(q1@env, list2env(list(a = 1))) }) @@ -77,12 +77,7 @@ testthat::test_that("eval_code works with quoted code block", { testthat::expect_equal( q1@code, - as.expression( - quote({ - a <- 1 - b <- 2 - }) - ) + "a <- 1\nb <- 2" ) testthat::expect_equal(q1@env, list2env(list(a = 1, b = 2))) }) @@ -98,13 +93,7 @@ testthat::test_that("an error when calling eval_code returns a qenv.error object testthat::expect_s3_class(q, "qenv.error") testthat::expect_equal( unname(q$trace), - as.expression( - c( - quote(x <- 1), - quote(y <- 2), - quote(z <- w * x) - ) - ) + c("x <- 1", "y <- 2", "z <- w * x") ) testthat::expect_equal(q$message, "object 'w' not found \n when evaluating qenv code:\nz <- w * x") }) @@ -129,7 +118,7 @@ testthat::test_that("a warning when calling eval_code returns a qenv object whic testthat::test_that("eval_code with a vector of code produces one warning element per code element", { q <- eval_code(new_qenv(), c("x <- 1", "y <- 1", "warning('warn1')")) - testthat::expect_equal(c("", "", "> warn1\n"), q@warnings) + testthat::expect_equal(c("> warn1\n"), q@warnings) }) diff --git a/tests/testthat/test-qenv_get_code.R b/tests/testthat/test-qenv_get_code.R index e7841221..e60ac5f2 100644 --- a/tests/testthat/test-qenv_get_code.R +++ b/tests/testthat/test-qenv_get_code.R @@ -13,13 +13,16 @@ testthat::test_that("get_code returns code elements being code-blocks as charact z <- 5 }) ) - testthat::expect_equal(get_code(q), c("x <- 1", "y <- x", "z <- 5")) + testthat::expect_equal(get_code(q), c("x <- 1", "y <- x\nz <- 5")) }) -testthat::test_that("get_code returns code (unparsed) of qenv object if deparse = FALSE", { +testthat::test_that("get_code returns expression of qenv object if deparse = FALSE", { q <- new_qenv(list2env(list(x = 1)), code = quote(x <- 1)) q <- eval_code(q, quote(y <- x)) - testthat::expect_equal(get_code(q, deparse = FALSE), q@code) + testthat::expect_equivalent( + toString(get_code(q, deparse = FALSE)), + toString(parse(text = q@code, keep.source = TRUE)) + ) }) testthat::test_that("get_code called with qenv.error returns error with trace in error message", { @@ -34,6 +37,6 @@ testthat::test_that("get_code called with qenv.error returns error with trace in testthat::expect_equal(class(code), c("validation", "try-error", "simpleError", "error", "condition")) testthat::expect_equal( code$message, - "object 'v' not found \n when evaluating qenv code:\nw <- v\n\ntrace: \n x <- 1\n y <- x\n w <- v\n" + "object 'v' not found \n when evaluating qenv code:\nw <- v\n\ntrace: \n c(\"x <- 1\", \"y <- x\", \"w <- v\")\n" ) }) diff --git a/tests/testthat/test-qenv_join.R b/tests/testthat/test-qenv_join.R index 42874bf1..ed8505d3 100644 --- a/tests/testthat/test-qenv_join.R +++ b/tests/testthat/test-qenv_join.R @@ -8,7 +8,7 @@ testthat::test_that("Joining two identical qenvs outputs the same object", { q <- join(q1, q2) testthat::expect_equal(q@env, env) - testthat::expect_identical(q@code, as.expression(quote(iris1 <- iris))) + testthat::expect_identical(q@code, "iris1 <- iris") testthat::expect_identical(q@id, q1@id) }) @@ -22,7 +22,7 @@ testthat::test_that("Joining two independent qenvs results in object having comb testthat::expect_equal(q@env, list2env(list(iris1 = iris, mtcars1 = mtcars))) testthat::expect_identical( q@code, - as.expression(c(quote(iris1 <- iris), quote(mtcars1 <- mtcars))) + c("iris1 <- iris", "mtcars1 <- mtcars") ) testthat::expect_identical(q@id, c(q1@id, q2@id)) }) @@ -42,9 +42,9 @@ testthat::test_that("Joined qenv does not duplicate common code", { testthat::expect_identical( q@code, - as.expression(c(quote(iris1 <- iris), quote(mtcars1 <- mtcars), quote(mtcars2 <- mtcars))) + c("iris1 <- iris\nmtcars1 <- mtcars", "mtcars2 <- mtcars") ) - testthat::expect_identical(q@id, c(q1@id, q2@id[3])) + testthat::expect_identical(q@id, c(q1@id, q2@id[2])) }) testthat::test_that("Not able to join two qenvs if any of the shared objects changed", { @@ -77,9 +77,7 @@ testthat::test_that("join does not duplicate code but adds only extra code", { testthat::expect_identical( q@code, - as.expression( - c(quote(iris1 <- iris), quote(mtcars1 <- mtcars), quote(iris2 <- iris), quote(mtcars2 <- mtcars)) - ) + c("iris1 <- iris\nmtcars1 <- mtcars", "iris2 <- iris", "mtcars2 <- mtcars") ) testthat::expect_equal( @@ -87,7 +85,7 @@ testthat::test_that("join does not duplicate code but adds only extra code", { list(iris1 = iris, iris2 = iris, mtcars1 = mtcars, mtcars2 = mtcars) ) - testthat::expect_identical(q@id, c(q1@id, q2@id[3])) + testthat::expect_identical(q@id, c(q1@id, q2@id[2])) }) testthat::test_that("Not possible to join qenvs which share some code when one of the shared object was modified", { @@ -111,7 +109,7 @@ testthat::test_that("qenv objects are mergeable if they don't share any code (id cq <- join(q1, q2) testthat::expect_s4_class(cq, "qenv") testthat::expect_equal(cq@env, list2env(list(a1 = 1))) - testthat::expect_identical(cq@code, as.expression(c(quote(a1 <- 1), quote(a1 <- 1)))) + testthat::expect_identical(cq@code, c("a1 <- 1", "a1 <- 1")) testthat::expect_identical(cq@id, c(q1@id, q2@id)) }) @@ -126,7 +124,7 @@ testthat::test_that("qenv objects are mergeable if they share common initial qen testthat::expect_equal(cq@env, list2env(list(a1 = 1, b1 = 2, a2 = 3))) testthat::expect_identical( cq@code, - as.expression(c(quote(a1 <- 1), quote(a2 <- 3), quote(b1 <- 2))) + c("a1 <- 1", "a2 <- 3", "b1 <- 2") ) testthat::expect_identical(cq@id, union(q1@id, q2@id)) }) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index ffbfbe07..52da288c 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -1,64 +1,105 @@ -testthat::test_that("remove_enclosing_curly_braces errors if argument is not character", { - testthat::expect_error(remove_enclosing_curly_braces(quote(x <- 1)), "Must be of type 'character") -}) +testthat::test_that("dev_suppress function supress printing plot on IDE", { + expect_no_error(dev_suppress(plot(1:10))) -testthat::test_that("remove_enclosing_curly_braces returns argument if it has length 0", { - testthat::expect_equal(remove_enclosing_curly_braces(character(0)), character(0)) -}) + initial_pdf_count <- sum(dev.list()) + dev_suppress(plot(1:10)) + final_pdf_count <- sum(dev.list()) -testthat::test_that("remove_enclosing_curly_braces only splits string on \n if no enclosing curly brackets", { - testthat::expect_equal(remove_enclosing_curly_braces("abc"), "abc") - testthat::expect_equal(remove_enclosing_curly_braces("abc\n def\n "), c("abc", " def", " ")) - testthat::expect_equal(remove_enclosing_curly_braces("{\nABC\n}A"), c("{", "ABC", "}A")) - testthat::expect_equal(remove_enclosing_curly_braces("{\nABC\nDEF\n A }"), c("{", "ABC", "DEF", " A }")) + expect_equal(final_pdf_count, initial_pdf_count, label = "The PDF device should be closed after calling dev_suppress") }) -testthat::test_that("remove_enclosing_curly_braces removes enclosing curly brackets", { - testthat::expect_equal(remove_enclosing_curly_braces("{\nA\n}"), "A") - testthat::expect_equal(remove_enclosing_curly_braces("{ \nA\n}"), "A") - testthat::expect_equal(remove_enclosing_curly_braces("{\nA\n} "), "A") - testthat::expect_equal(remove_enclosing_curly_braces(" { \nA\n }"), "A") -}) -testthat::test_that("remove_enclosing_curly_braces concatenates input character vector", { - testthat::expect_equal(remove_enclosing_curly_braces(c("ABC", "DEF")), c("ABC", "DEF")) - testthat::expect_equal(remove_enclosing_curly_braces(c("{\n ABC", " DEF\n}")), c("ABC", "DEF")) - testthat::expect_equal(remove_enclosing_curly_braces(c("{\n ABC\n}", " DEF")), c("{", " ABC", "}", " DEF")) -}) +# lang2calls ------------------------------------------------------------------------------------------------------ +testthat::test_that("lang2calls returns list of calls given a language object", { + expr1 <- expression( + i <- iris + ) + expr2 <- expression( + i <- iris, + m <- mtcars + ) + call1 <- quote( + i <- iris + ) + call2 <- quote({ + i <- iris + m <- mtcars + }) -testthat::test_that( - desc = "remove_enclosing_curly_braces containing enclosing brackets and only blank lines returns blank lines", - code = { - testthat::expect_equal(remove_enclosing_curly_braces("{\n\n\n}"), c("", "")) - testthat::expect_equal(remove_enclosing_curly_braces(" { \n\n } "), "") - } -) + testthat::expect_true(is.list(lang2calls(expr1)) && all(vapply(lang2calls(expr1), is.call, logical(1L)))) + testthat::expect_true(is.list(lang2calls(expr2)) && all(vapply(lang2calls(expr2), is.call, logical(1L)))) + testthat::expect_true(is.list(lang2calls(call1)) && all(vapply(lang2calls(call1), is.call, logical(1L)))) + testthat::expect_true(is.list(lang2calls(call2)) && all(vapply(lang2calls(call2), is.call, logical(1L)))) +}) -testthat::test_that("remove_enclosing_curly_braces removes 4 spaces from lines enclosed by brackets if they exist", { - testthat::expect_equal(remove_enclosing_curly_braces("{\n A\n}"), "A") - testthat::expect_equal( - remove_enclosing_curly_braces("{\nA\n B\n C\n D\n E \n F\n \n}"), - c("A", " B", " C", " D", "E ", "F", "") +testthat::test_that("lang2calls returns list of calls given a list of language objects", { + exprlist <- list( + expression(i <- iris), + expression( + i <- iris, + m <- mtcars + ) + ) + calllist <- list( + quote(i <- iris), + quote({ + i <- iris + m <- mtcars + }) ) -}) -test_that("dev_suppress function supress printing plot on IDE", { - expect_no_error(dev_suppress(plot(1:10))) + testthat::expect_true(is.list(lang2calls(exprlist)) && all(vapply(lang2calls(exprlist), is.call, logical(1L)))) + testthat::expect_true(is.list(lang2calls(calllist)) && all(vapply(lang2calls(calllist), is.call, logical(1L)))) +}) - initial_pdf_count <- sum(dev.list()) - dev_suppress(plot(1:10)) - final_pdf_count <- sum(dev.list()) +testthat::test_that("lang2calls returns atomics and symbols wrapped in list", { + testthat::expect_identical(lang2calls("x"), list("x")) + testthat::expect_identical(lang2calls(as.symbol("x")), list(as.symbol("x"))) - expect_equal(final_pdf_count, initial_pdf_count, label = "The PDF device should be closed after calling dev_suppress") + testthat::expect_identical(lang2calls(list("x")), list("x")) + testthat::expect_identical(lang2calls(list(as.symbol("x"))), list(as.symbol("x"))) }) -testthat::test_that("format expression concatenates results of remove_enclosing_curly_braces", { - code_list <- list( - quote("x <- 1"), - quote({ - y <- 1 - z <- 1 + +testthat::test_that( + "format_expression turns expression/calls or lists thereof into character strings without curly brackets", + { + expr1 <- expression({ + i <- iris + m <- mtcars }) - ) - expect_equal(format_expression(code_list), c("x <- 1", "y <- 1", "z <- 1")) -}) + expr2 <- expression( + i <- iris, + m <- mtcars + ) + expr3 <- list( + expression(i <- iris), + expression(m <- mtcars) + ) + cll1 <- quote({ + i <- iris + m <- mtcars + }) + cll2 <- list( + quote(i <- iris), + quote(m <- mtcars) + ) + + # function definition + fundef <- quote( + format_expression <- function(x) { + x + x + return(x) + } + ) + + testthat::expect_identical(format_expression(expr1), "i <- iris\nm <- mtcars") + testthat::expect_identical(format_expression(expr2), "i <- iris\nm <- mtcars") + testthat::expect_identical(format_expression(expr3), "i <- iris\nm <- mtcars") + testthat::expect_identical(format_expression(cll1), "i <- iris\nm <- mtcars") + testthat::expect_identical(format_expression(cll2), "i <- iris\nm <- mtcars") + testthat::expect_identical( + format_expression(fundef), "format_expression <- function(x) {\n x + x\n return(x)\n}" + ) + } +) diff --git a/vignettes/qenv.Rmd b/vignettes/qenv.Rmd index 5b0a8c73..7ccb3656 100644 --- a/vignettes/qenv.Rmd +++ b/vignettes/qenv.Rmd @@ -3,7 +3,7 @@ title: "qenv" author: "NEST coreDev" output: rmarkdown::html_vignette vignette: > - %\VignetteIndexEntry{qenv} + %\VignetteIndexEntry{`qenv`} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} ---