Movatterモバイル変換


[0]ホーム

URL:


teal.code coverage - 92.84%

1
# get_code_dependency ----
2
3
#' Get code dependency of an object
4
#'
5
#' Extract subset of code required to reproduce specific object(s), including code producing side-effects.
6
#'
7
#' Given a character vector with code, this function will extract the part of the code responsible for creating
8
#' the variables specified by `names`.
9
#' This includes the final call that creates the variable(s) in question as well as all _parent calls_,
10
#' _i.e._ calls that create variables used in the final call and their parents, etc.
11
#' Also included are calls that create side-effects like establishing connections.
12
#'
13
#' It is assumed that object dependency is established by using three assignment operators: `<-`, `=`, and `->` .
14
#' Other assignment methods (`assign`, `<<-`) or non-standard-evaluation methods are not supported.
15
#'
16
#' Side-effects are not detected automatically and must be marked in the code.
17
#' Add `# @linksto object` at the end of a line where a side-effect occurs to specify that this line is required
18
#' to reproduce a variable called `object`.
19
#'
20
#' @param code `character` with the code.
21
#' @param names `character` vector of object names.
22
#' @param check_code_names `logical(1)` flag specifying if a warning for non-existing names should be displayed.
23
#'
24
#' @return Character vector, a subset of `code`.
25
#' Note that subsetting is actually done on the calls `code`, not necessarily on the elements of the vector.
26
#'
27
#' @keywords internal
28
get_code_dependency <- function(code, names, check_code_names = TRUE) {
2998x
  checkmate::assert_list(code, "character")
3098x
  checkmate::assert_character(names, any.missing = FALSE)
31
3298x
  graph <- lapply(code, attr, "dependency")
33
3498x
  if (check_code_names) {
3597x
    symbols <- unlist(lapply(graph, function(call) {
36267x
      ind <- match("<-", call, nomatch = length(call) + 1L)
37267x
      call[seq_len(ind - 1L)]
38
    }))
39
4097x
    if (!all(names %in% unique(symbols))) {
418x
      warning("Object(s) not found in code: ", toString(setdiff(names, symbols)), ".", call. = FALSE)
42
    }
43
  }
44
4598x
  if (length(code) == 0) {
461x
    return(code)
47
  }
48
4997x
  ind <- unlist(lapply(names, function(x) graph_parser(x, graph)))
50
5197x
  lib_ind <- detect_libraries(graph)
52
5397x
  code_ids <- sort(unique(c(lib_ind, ind)))
5497x
  code[code_ids]
55
}
56
57
#' Locate function call token
58
#'
59
#' Determine which row of parsed data is specific `SYMBOL_FUNCTION_CALL` token.
60
#'
61
#' Useful for determining occurrence of `assign` or `data` functions in an input call.
62
#'
63
#' @param call_pd `data.frame` as returned by `extract_calls()`
64
#' @param text `character(1)` to look for in `text` column of `call_pd`
65
#'
66
#' @return
67
#' Single integer specifying row in `call_pd` where `token` is `SYMBOL_FUNCTION_CALL` and `text` is `text`.
68
#' 0 if not found.
69
#'
70
#' @keywords internal
71
#' @noRd
72
find_call <- function(call_pd, text) {
73825x
  checkmate::check_data_frame(call_pd)
74825x
  checkmate::check_names(call_pd, must.include = c("token", "text"))
75825x
  checkmate::check_string(text)
76
77825x
  ans <- which(call_pd$token == "SYMBOL_FUNCTION_CALL" & call_pd$text == text)
78825x
  if (length(ans)) {
7911x
    ans
80
  } else {
81814x
    0L
82
  }
83
}
84
85
#' Split the result of `utils::getParseData()` into separate calls
86
#'
87
#' @param pd (`data.frame`) A result of `utils::getParseData()`.
88
#'
89
#' @return
90
#' A `list` of `data.frame`s.
91
#' Each element is a subset of `pd` corresponding to one call in the original code from which `pd` was obtained.
92
#' Only four columns (`"token"`, `"text"`, `"id"`, `"parent"`) are kept, the rest is discarded.
93
#'
94
#' @keywords internal
95
#' @noRd
96
extract_calls <- function(pd) {
971074x
  calls <- lapply(
981074x
    pd[pd$parent == 0 & (pd$token != "COMMENT" | grepl("@linksto", pd$text, fixed = TRUE)), "id"],
991074x
    function(parent) {
1001223x
      rbind(
1011223x
        pd[pd$id == parent, ],
1021223x
        get_children(pd = pd, parent = parent)
103
      )
104
    }
105
  )
1061074x
  calls <- Filter(function(call) !(nrow(call) == 1 && call$token == "';'"), calls)
1071074x
  calls <- Filter(Negate(is.null), calls)
1081074x
  calls <- fix_shifted_comments(calls)
1091074x
  calls <- remove_custom_assign(calls, c(":="))
1101074x
  fix_arrows(calls)
111
}
112
113
#' @keywords internal
114
#' @noRd
115
get_children <- function(pd, parent) {
11612964x
  idx_children <- abs(pd$parent) == parent
11712964x
  children <- pd[idx_children, ]
11812964x
  if (nrow(children) == 0) {
1197401x
    return(NULL)
120
  }
121
1225563x
  if (parent > 0) {
1235563x
    do.call(rbind, c(list(children), lapply(children$id, get_children, pd = pd)))
124
  }
125
}
126
127
#' Fixes edge case of comments being shifted to the next call.
128
#' @keywords internal
129
#' @noRd
130
fix_shifted_comments <- function(calls) {
131
  # If the first or the second token is a @linksto COMMENT,
132
  #  then it belongs to the previous call.
1331074x
  if (length(calls) >= 2) {
134107x
    for (i in 2:length(calls)) {
135186x
      comment_idx <- grep("@linksto", calls[[i]][, "text"])
136186x
      if (isTRUE(comment_idx[1] <= 2)) {
1379x
        calls[[i - 1]] <- rbind(
1389x
          calls[[i - 1]],
1399x
          calls[[i]][comment_idx[1], ]
140
        )
1419x
        calls[[i]] <- calls[[i]][-comment_idx[1], ]
142
      }
143
    }
144
  }
1451074x
  Filter(nrow, calls)
146
}
147
148
#' Fixes edge case of custom assignments operator being treated as assignment.
149
#'
150
#' @param exclude (`character`) custom assignment operators to be excluded
151
#' @keywords internal
152
#' @noRd
153
remove_custom_assign <- function(calls, exclude = NULL) {
1541074x
  checkmate::assert_list(calls)
1551074x
  checkmate::assert_character(exclude, null.ok = TRUE)
1561074x
  lapply(calls, function(call) {
1571216x
    if (!is.null(exclude)) {
1581216x
      call[!(call$token == "LEFT_ASSIGN" & call$text %in% exclude), ]
159
    } else {
160!
      call
161
    }
162
  })
163
}
164
165
#' Fixes edge case of `<-` assignment operator being called as function,
166
#' which is \code{`<-`(y,x)} instead of traditional `y <- x`.
167
#' @keywords internal
168
#' @noRd
169
fix_arrows <- function(calls) {
1701074x
  checkmate::assert_list(calls)
1711074x
  lapply(calls, function(call) {
1721216x
    sym_fun <- call$token == "SYMBOL_FUNCTION_CALL"
1731216x
    call[sym_fun, ] <- sub_arrows(call[sym_fun, ])
1741216x
    call
175
  })
176
}
177
178
#' Execution of assignment operator substitutions for a call.
179
#' @keywords internal
180
#' @noRd
181
sub_arrows <- function(call) {
1821216x
  checkmate::assert_data_frame(call)
1831216x
  map <- data.frame(
1841216x
    row.names = c("<-", "<<-", "="),
1851216x
    token = rep("LEFT_ASSIGN", 3),
1861216x
    text = rep("<-", 3)
187
  )
1881216x
  sub_ids <- call$text %in% rownames(map)
1891216x
  call[sub_ids, c("token", "text")] <- map[call$text[sub_ids], ]
1901216x
  call
191
}
192
193
# code_graph ----
194
195
#' Extract object occurrence
196
#'
197
#' Extracts objects occurrence within calls passed by `pd`.
198
#' Also detects which objects depend on which within a call.
199
#'
200
#' @param pd `data.frame`;
201
#'  one of the results of `utils::getParseData()` split into subsets representing individual calls;
202
#'  created by `extract_calls()` function
203
#'
204
#' @return
205
#' A character vector listing names of objects that depend on this call
206
#' and names of objects that this call depends on.
207
#' Dependencies are listed after the `"<-"` string, e.g. `c("a", "<-", "b", "c")` means that in this call object `a`
208
#' depends on objects `b` and `c`.
209
#' If a call is tagged with `@linksto a`, then object `a` is understood to depend on that call.
210
#'
211
#' @keywords internal
212
#' @noRd
213
extract_occurrence <- function(pd) {
214414x
  is_in_function <- function(x) {
215
    # If an object is a function parameter,
216
    # then in calls_pd there is a `SYMBOL_FORMALS` entry for that object.
217403x
    function_id <- x[x$token == "FUNCTION", "parent"]
218403x
    if (length(function_id)) {
21920x
      x$id %in% get_children(x, function_id[1])$id
220
    } else {
221383x
      rep(FALSE, nrow(x))
222
    }
223
  }
224414x
  in_parenthesis <- function(x) {
225315x
    if (any(x$token %in% c("LBB", "'['"))) {
22622x
      id_start <- min(x$id[x$token %in% c("LBB", "'['")])
22722x
      id_end <- min(x$id[x$token == "']'"])
22822x
      x$text[x$token == "SYMBOL" & x$id > id_start & x$id < id_end]
229
    }
230
  }
231
232
  # Handle data(object)/data("object")/data(object, envir = ) independently.
233414x
  data_call <- find_call(pd, "data")
234414x
  if (data_call) {
2353x
    sym <- pd[data_call + 1, "text"]
2363x
    return(c(gsub("^['\"]|['\"]$", "", sym), "<-"))
237
  }
238
  # Handle assign(x = ).
239411x
  assign_call <- find_call(pd, "assign")
240411x
  if (assign_call) {
241
    # Check if parameters were named.
242
    # "','" is for unnamed parameters, where "SYMBOL_SUB" is for named.
243
    # "EQ_SUB" is for `=` appearing after the name of the named parameter.
2448x
    if (any(pd$token == "SYMBOL_SUB")) {
2454x
      params <- pd[pd$token %in% c("SYMBOL_SUB", "','", "EQ_SUB"), "text"]
246
      # Remove sequence of "=", ",".
2474x
      if (length(params > 1)) {
2484x
        remove <- integer(0)
2494x
        for (i in 2:length(params)) {
25020x
          if (params[i - 1] == "=" && params[i] == ",") {
2514x
            remove <- c(remove, i - 1, i)
252
          }
253
        }
2543x
        if (length(remove)) params <- params[-remove]
255
      }
2564x
      pos <- match("x", setdiff(params, ","), nomatch = match(",", params, nomatch = 0))
2574x
      if (!pos) {
258!
        return(character(0L))
259
      }
260
      # pos is indicator of the place of 'x'
261
      # 1. All parameters are named, but none is 'x' - return(character(0L))
262
      # 2. Some parameters are named, 'x' is in named parameters: match("x", setdiff(params, ","))
263
      # - check "x" in params being just a vector of named parameters.
264
      # 3. Some parameters are named, 'x' is not in named parameters
265
      # - check first appearance of "," (unnamed parameter) in vector parameters.
266
    } else {
267
      # Object is the first entry after 'assign'.
2684x
      pos <- 1
269
    }
2708x
    sym <- pd[assign_call + pos, "text"]
2718x
    return(c(gsub("^['\"]|['\"]$", "", sym), "<-"))
272
  }
273
274
  # What occurs in a function body is not tracked.
275403x
  x <- pd[!is_in_function(pd), ]
276403x
  sym_cond <- which(x$token %in% c("SPECIAL", "SYMBOL", "SYMBOL_FUNCTION_CALL"))
277403x
  sym_fc_cond <- which(x$token == "SYMBOL_FUNCTION_CALL")
278
279403x
  if (length(sym_cond) == 0) {
28017x
    return(character(0L))
281
  }
282
  # Watch out for SYMBOLS after $ and @. For x$a x@a: x is object, a is not.
283
  # For x$a, a's ID is $'s ID-2 so we need to remove all IDs that have ID = $ID - 2.
284386x
  dollar_ids <- x[x$token %in% c("'$'", "'@'"), "id"]
285386x
  if (length(dollar_ids)) {
28613x
    object_ids <- x[sym_cond, "id"]
28713x
    after_dollar <- object_ids[(object_ids - 2) %in% dollar_ids]
28813x
    sym_cond <- setdiff(sym_cond, which(x$id %in% after_dollar))
289
  }
290
291386x
  assign_cond <- grep("ASSIGN", x$token)
292386x
  if (!length(assign_cond)) {
29371x
    return(c("<-", unique(x[sym_cond, "text"])))
294
  }
295
296
  # For cases like 'eval(expression(c <- b + 2))' removes 'eval(expression('.
297315x
  sym_cond <- sym_cond[!(sym_cond < min(assign_cond) & sym_cond %in% sym_fc_cond)]
298
299
  # If there was an assignment operation detect direction of it.
300315x
  if (unique(x$text[assign_cond]) == "->") { # What if there are 2 assignments: e.g. a <- b -> c.
3011x
    sym_cond <- rev(sym_cond)
302
  }
303
304315x
  after <- match(min(x$id[assign_cond]), sort(x$id[c(min(assign_cond), sym_cond)])) - 1
305315x
  ans <- append(x[sym_cond, "text"], "<-", after = max(1, after))
306315x
  ans <- move_functions_after_arrow(ans, unique(x[sym_fc_cond, "text"]))
307315x
  roll <- in_parenthesis(pd)
308315x
  if (length(roll)) {
309
    # detect elements appeared in parenthesis and move them on RHS
310
    # but only their first appearance
311
    # as the same object can appear as regular object and the one used in parenthesis
3128x
    result <- ans
3138x
    for (elem in roll) {
31411x
      idx <- which(result == elem)[1]
31511x
      if (!is.na(idx)) {
3169x
        result <- result[-idx]
317
      }
318
    }
3198x
    c(result, roll)
320
  } else {
321307x
    ans
322
  }
323
}
324
325
#' Moves function names to the right side of dependency graph
326
#'
327
#' Changes status of the function call from dependent to dependency if occurs in the lhs.
328
#' Technically, it means to move function names after the dependency operator.
329
#' For example, for `attributes(a) <- b` the dependency graph should look like `c("a", "<-", "b", "attributes")`.
330
#'
331
#' @param ans `character` vector of object names in dependency graph.
332
#' @param functions `character` vector of function names.
333
#'
334
#' @return
335
#' A character vector.
336
#' @keywords internal
337
#' @noRd
338
move_functions_after_arrow <- function(ans, functions) {
339315x
  arrow_pos <- which(ans == "<-")
340315x
  if (length(arrow_pos) == 0) {
341!
    return(ans)
342
  }
343315x
  if (length(functions) == 0) {
344253x
    return(ans)
345
  }
34662x
  ans_pre <- ans[1:arrow_pos]
347
  # it's setdiff but without the removal of duplicates
348
  # do not use setdiff(ans_pre, functions)
349
  # as it removes duplicates from ans_pre even if they do not appear in functions
350
  # check setdiff(c("A", "A"), "B") - gives "A", where we want to keep c("A", "A")
35162x
  for (fun in functions) {
3529x
    if (any(ans_pre == fun)) ans_pre <- ans_pre[-match(fun, ans_pre)]
353
  }
35462x
  after_arrow <- if (arrow_pos < length(ans)) {
35560x
    ans[(arrow_pos + 1):length(ans)]
356
  }
35762x
  c(ans_pre, after_arrow)
358
}
359
360
#' Extract side effects
361
#'
362
#' Extracts all object names from the code that are marked with `@linksto` tag.
363
#'
364
#' The code may contain functions calls that create side effects, e.g. modify the environment.
365
#' Static code analysis may be insufficient to determine which objects are created or modified by such a function call.
366
#' The `@linksto` comment tag is introduced to mark a call as having a (side) effect on one or more objects.
367
#' With this tag a complete object dependency structure can be established.
368
#' Read more about side effects and the usage of `@linksto` tag in [`get_code_dependencies()`] function.
369
#'
370
#' @param pd `data.frame`;
371
#'  one of the results of `utils::getParseData()` split into subsets representing individual calls;
372
#'  created by `extract_calls()` function
373
#'
374
#' @return
375
#' A character vector of names of objects
376
#' depending a call tagged with `@linksto` in a corresponding element of `pd`.
377
#'
378
#' @keywords internal
379
#' @noRd
380
extract_side_effects <- function(pd) {
381401x
  linksto <- grep("@linksto", pd[pd$token == "COMMENT", "text"], value = TRUE)
382401x
  unlist(strsplit(sub("\\s*#.*@linksto\\s+", "", linksto), "\\s+"))
383
}
384
385
#' @param parsed_code results of `parse(text = code, keep.source = TRUE` (parsed text)
386
#' @keywords internal
387
#' @noRd
388
extract_dependency <- function(parsed_code) {
389437x
  full_pd <- normalize_pd(utils::getParseData(parsed_code))
390437x
  reordered_full_pd <- extract_calls(full_pd)
391
392
  # Early return on empty code
393437x
  if (length(reordered_full_pd) == 0L) {
39436x
    return(NULL)
395
  }
396
397401x
  if (length(parsed_code) == 0L) {
3981x
    return(extract_side_effects(reordered_full_pd[[1]]))
399
  }
400400x
  expr_ix <- lapply(parsed_code[[1]], class) == "{"
401
402
  # Build queue of expressions to parse individually
403400x
  queue <- list()
404400x
  parsed_code_list <- if (all(!expr_ix)) {
405392x
    list(parsed_code)
406
  } else {
4078x
    queue <- as.list(parsed_code[[1]][expr_ix])
4088x
    new_list <- parsed_code[[1]]
4098x
    new_list[expr_ix] <- NULL
4108x
    list(parse(text = as.expression(new_list), keep.source = TRUE))
411
  }
412
413400x
  while (length(queue) > 0) {
41422x
    current <- queue[[1]]
41522x
    queue <- queue[-1]
41622x
    if (identical(current[[1L]], as.name("{"))) {
4178x
      queue <- append(queue, as.list(current)[-1L])
418
    } else {
41914x
      parsed_code_list[[length(parsed_code_list) + 1]] <- parse(text = as.expression(current), keep.source = TRUE)
420
    }
421
  }
422
423400x
  parsed_occurences <- lapply(
424400x
    parsed_code_list,
425400x
    function(parsed_code) {
426414x
      pd <- normalize_pd(utils::getParseData(parsed_code))
427414x
      reordered_pd <- extract_calls(pd)
428414x
      if (length(reordered_pd) > 0) {
429
        # extract_calls is needed to reorder the pd so that assignment operator comes before symbol names
430
        # extract_calls is needed also to substitute assignment operators into specific format with fix_arrows
431
        # extract_calls is needed to omit empty calls that contain only one token `"';'"`
432
        # This cleaning is needed as extract_occurrence assumes arrows are fixed, and order is different
433
        # than in original pd
434414x
        extract_occurrence(reordered_pd[[1]])
435
      }
436
    }
437
  )
438
439
  # Merge results together
440400x
  result <- Reduce(
441400x
    function(u, v) {
442414x
      ix <- if ("<-" %in% v) min(which(v == "<-")) else 0
443414x
      u$left_side <- c(u$left_side, v[seq_len(max(0, ix - 1))])
444414x
      u$right_side <- c(
445414x
        u$right_side,
446414x
        if (ix == length(v)) character(0L) else v[seq(ix + 1, max(ix + 1, length(v)))]
447
      )
448414x
      u
449
    },
450400x
    init = list(left_side = character(0L), right_side = character(0L)),
451400x
    x = parsed_occurences
452
  )
453
454400x
  c(extract_side_effects(reordered_full_pd[[1]]), result$left_side, "<-", result$right_side)
455
}
456
457
# graph_parser ----
458
459
#' Return the indices of calls needed to reproduce an object
460
#'
461
#' @param x The name of the object to return code for.
462
#' @param graph A result of `code_graph()`.
463
#'
464
#' @return
465
#' Integer vector of indices that can be applied to `graph` to obtain all calls required to reproduce object `x`.
466
#'
467
#' @keywords internal
468
#' @noRd
469
graph_parser <- function(x, graph) {
470
  # x occurrences (lhs)
471398x
  occurrence <- vapply(
472398x
    graph, function(call) {
473667x
      ind <- match("<-", call, nomatch = length(call) + 1L)
474667x
      x %in% call[seq_len(ind - 1L)]
475
    },
476398x
    logical(1)
477
  )
478
479
  # x-dependent objects (rhs)
480398x
  dependencies <- lapply(graph[occurrence], function(call) {
481184x
    ind <- match("<-", call, nomatch = 0L)
482184x
    call[(ind + 1L):length(call)]
483
  })
484398x
  dependencies <- setdiff(unlist(dependencies), x)
485
486398x
  dependency_occurrences <- lapply(dependencies, function(dependency) {
487
    # track down dependencies and where they occur on the lhs in previous calls
488286x
    last_x_occurrence <- max(which(occurrence))
489286x
    reduced_graph <- utils::head(graph[seq_len(last_x_occurrence)], -1)
490286x
    c(graph_parser(dependency, reduced_graph), last_x_occurrence)
491
  })
492
493398x
  sort(unique(c(which(occurrence), unlist(dependency_occurrences))))
494
}
495
496
497
# default_side_effects --------------------------------------------------------------------------------------------
498
499
#' Detect library calls
500
#'
501
#' Detects `library()` and `require()` function calls.
502
#'
503
#' @param `graph` the dependency graph, result of `lapply(code, attr, "dependency")`
504
#'
505
#' @return
506
#' Integer vector of indices that can be applied to `graph` to obtain all calls containing
507
#' `library()` or `require()` calls that are always returned for reproducibility.
508
#'
509
#' @keywords internal
510
#' @noRd
511
detect_libraries <- function(graph) {
51297x
  defaults <- c("library", "require")
513
51497x
  which(
51597x
    unlist(
51697x
      lapply(
51797x
        graph, function(x) {
518268x
          any(grepl(pattern = paste(defaults, collapse = "|"), x = x))
519
        }
520
      )
521
    )
522
  )
523
}
524
525
526
# utils -----------------------------------------------------------------------------------------------------------
527
528
529
#' Normalize parsed data removing backticks from symbols
530
#'
531
#' @param pd `data.frame` resulting from `utils::getParseData()` call.
532
#'
533
#' @return `data.frame` with backticks removed from `text` column for `SYMBOL` tokens.
534
#'
535
#' @keywords internal
536
#' @noRd
537
normalize_pd <- function(pd) {
538
  # Remove backticks from SYMBOL tokens
5391074x
  symbol_index <- grepl("^SYMBOL.*$", pd$token)
5401074x
  pd[symbol_index, "text"] <- gsub("^`(.*)`$", "\\1", pd[symbol_index, "text"])
541
5421074x
  pd
543
}
544
545
546
# split_code ------------------------------------------------------------------------------------------------------
547
548
549
#' Get line/column in the source where the calls end
550
#'
551
#'
552
#' @param code `character(1)`
553
#'
554
#' @return `matrix` with `colnames = c("line", "col")`
555
#'
556
#' @keywords internal
557
#' @noRd
558
get_call_breaks <- function(code) {
559223x
  parsed_code <- parse(text = code, keep.source = TRUE)
560223x
  pd <- utils::getParseData(parsed_code)
561223x
  pd <- normalize_pd(pd)
562223x
  pd <- pd[pd$token != "';'", ]
563223x
  call_breaks <- t(sapply(
564223x
    extract_calls(pd),
565223x
    function(x) {
566401x
      matrix(c(max(x$line2), max(x$col2[x$line2 == max(x$line2)])))
567
    }
568
  ))
569223x
  call_breaks <- call_breaks[-nrow(call_breaks), , drop = FALSE] # breaks in between needed only
570223x
  if (nrow(call_breaks) == 0L) {
571122x
    call_breaks <- matrix(numeric(0), ncol = 2)
572
  }
573223x
  colnames(call_breaks) <- c("line", "col")
574223x
  call_breaks
575
}
576
577
#' Split code by calls
578
#'
579
#' @param code `character` with the code.
580
#'
581
#' @return list of `character`s of the length equal to the number of calls in `code`.
582
#'
583
#' @keywords internal
584
#' @noRd
585
split_code <- function(code) {
586223x
  call_breaks <- get_call_breaks(code)
587223x
  if (nrow(call_breaks) == 0) {
588122x
    return(code)
589
  }
590101x
  call_breaks <- call_breaks[order(call_breaks[, "line"], call_breaks[, "col"]), , drop = FALSE]
591101x
  code_split <- strsplit(code, split = "\n", fixed = TRUE)[[1]]
592101x
  char_count_lines <- c(0, cumsum(sapply(code_split, nchar, USE.NAMES = FALSE) + 1), -1)[seq_along(code_split)]
593
594101x
  idx_start <- c(
595101x
    0, # first call starts in the beginning of src
596101x
    char_count_lines[call_breaks[, "line"]] + call_breaks[, "col"] + 1
597
  )
598101x
  idx_end <- c(
599101x
    char_count_lines[call_breaks[, "line"]] + call_breaks[, "col"],
600101x
    nchar(code) # last call end in the end of src
601
  )
602101x
  new_code <- substring(code, idx_start, idx_end)
603
604
  # line split happens before call terminator (it could be `;` or `\n`) and the terminator goes to the next line
605
  # we need to move remove leading and add \n instead when combining calls
606101x
  c(new_code[1], gsub("^[\t ]*(\n|;)", "", new_code[-1]))
607
}
1
#' Suppresses plot display in the IDE by opening a PDF graphics device
2
#'
3
#' This function opens a PDF graphics device using [`grDevices::pdf`] to suppress
4
#' the plot display in the IDE. The purpose of this function is to avoid opening graphic devices
5
#' directly in the IDE.
6
#'
7
#' @param x lazy binding which generates the plot(s)
8
#'
9
#' @details The function uses [`base::on.exit`] to ensure that the PDF graphics
10
#'          device is closed (using [`grDevices::dev.off`]) when the function exits,
11
#'          regardless of whether it exits normally or due to an error. This is necessary to
12
#'          clean up the graphics device properly and avoid any potential issues.
13
#'
14
#' @return No return value, called for side effects.
15
#'
16
#' @examples
17
#' dev_suppress(plot(1:10))
18
#' @export
19
dev_suppress <- function(x) {
20151x
  grDevices::pdf(nullfile())
21151x
  on.exit(grDevices::dev.off())
22151x
  force(x)
23
}
24
25
#' Separate calls
26
#'
27
#' Converts language object or lists of language objects to list of simple calls.
28
#'
29
#' @param x `language` object or a list of thereof
30
#' @return
31
#' Given a `call`, an `expression`, a list of `call`s or a list of `expression`s, returns a list of `calls`.
32
#' Symbols and atomic vectors (which may get mixed up in a list) are returned wrapped in list.
33
#' @examples
34
#' # use non-exported function from teal.code
35
#' lang2calls <- getFromNamespace("lang2calls", "teal.code")
36
#' expr <- expression(
37
#'   i <- iris,
38
#'   m <- mtcars
39
#' )
40
#' lang2calls(expr)
41
#' @keywords internal
42
lang2calls <- function(x) {
43293x
  if (is.atomic(x) || is.symbol(x)) {
449x
    return(list(x))
45
  }
46284x
  if (is.call(x)) {
47187x
    if (identical(as.list(x)[[1L]], as.symbol("{"))) {
4858x
      as.list(x)[-1L]
49
    } else {
50129x
      list(x)
51
    }
52
  } else {
5397x
    unlist(lapply(x, lang2calls), recursive = FALSE)
54
  }
55
}
56
57
#' Obtain warnings or messages from code slot
58
#'
59
#' @param object (`qenv`)
60
#' @param what (`warning` or `message`)
61
#' @return `character(1)` containing combined message or `NULL` when no warnings/messages
62
#' @keywords internal
63
get_warn_message_util <- function(object, what) {
6414x
  checkmate::matchArg(what, choices = c("warning", "message"))
6514x
  messages <- lapply(
6614x
    object@code,
6714x
    function(x) {
6824x
      unlist(lapply(
6924x
        attr(x, "outputs"),
7024x
        function(el) {
7120x
          if (inherits(el, what)) {
7220x
            sprintf("> %s", conditionMessage(el))
73
          }
74
        }
75
      ))
76
    }
77
  )
78
7914x
  idx_warn <- which(sapply(messages, function(x) !is.null(x) && !identical(x, "")))
8014x
  if (!any(idx_warn)) {
812x
    return(NULL)
82
  }
8312x
  messages <- messages[idx_warn]
8412x
  code <- object@code[idx_warn]
85
8612x
  lines <- mapply(
8712x
    warn = messages,
8812x
    expr = code,
8912x
    function(warn, expr) {
9020x
      sprintf("%s\nwhen running code:\n%s", trimws(warn), trimws(expr))
91
    }
92
  )
93
9412x
  sprintf(
9512x
    "~~~ %ss ~~~\n\n%s\n\n~~~ Trace ~~~\n\n%s",
9612x
    tools::toTitleCase(what),
9712x
    paste(lines, collapse = "\n\n"),
9812x
    paste(get_code(object), collapse = "\n")
99
  )
100
}
1
#' Get code from `qenv`
2
#'
3
#' @description
4
#' Retrieves the code stored in the `qenv`.
5
#'
6
#' @param object (`qenv`)
7
#' @param deparse (`logical(1)`) flag specifying whether to return code as `character` or `expression`.
8
#' @param ... internal usage, please ignore.
9
#' @param names (`character`) `r lifecycle::badge("experimental")` vector of object names to return the code for.
10
#' For more details see the "Extracting dataset-specific code" section.
11
#'
12
#' @section Extracting dataset-specific code:
13
#'
14
#' `get_code(object, names)` limits the returned code to contain only those lines needed to _create_
15
#' the requested objects. The code stored in the `qenv` is analyzed statically to determine
16
#' which lines the objects of interest depend upon. The analysis works well when objects are created
17
#' with standard infix assignment operators (see `?assignOps`) but it can fail in some situations.
18
#'
19
#' Consider the following examples:
20
#'
21
#' _Case 1: Usual assignments._
22
#' ```r
23
#' q1 <-
24
#'   within(qenv(), {
25
#'     foo <- function(x) {
26
#'       x + 1
27
#'     }
28
#'     x <- 0
29
#'     y <- foo(x)
30
#'   })
31
#' get_code(q1, names = "y")
32
#' ```
33
#' `x` has no dependencies, so `get_code(data, names = "x")` will return only the second call.\cr
34
#' `y` depends on `x` and `foo`, so `get_code(data, names = "y")` will contain all three calls.
35
#'
36
#' _Case 2: Some objects are created by a function's side effects._
37
#' ```r
38
#' q2 <-
39
#'   within(qenv(){
40
#'     foo <- function() {
41
#'       x <<- x + 1
42
#'     }
43
#'     x <- 0
44
#'     foo()
45
#'     y <- x
46
#'   })
47
#' get_code(q2, names = "y")
48
#' ```
49
#' Here, `y` depends on `x` but `x` is modified by `foo` as a side effect (not by reassignment)
50
#' and so `get_code(data, names = "y")` will not return the `foo()` call.\cr
51
#' To overcome this limitation, code dependencies can be specified manually.
52
#' Lines where side effects occur can be flagged by adding "`# @linksto <object name>`" at the end.\cr
53
#' Note that `within` evaluates code passed to `expr` as is and comments are ignored.
54
#' In order to include comments in code one must use the `eval_code` function instead.
55
#'
56
#' ```r
57
#' q3 <-
58
#'   eval_code(qenv(), "
59
#'     foo <- function() {
60
#'       x <<- x + 1
61
#'     }
62
#'     x <- 0
63
#'     foo() # @linksto x
64
#'     y <- x
65
#'   ")
66
#' get_code(q3, names = "y")
67
#' ```
68
#' Now the `foo()` call will be properly included in the code required to recreate `y`.
69
#'
70
#' Note that two functions that create objects as side effects, `assign` and `data`, are handled automatically.
71
#'
72
#' Here are known cases where manual tagging is necessary:
73
#' - non-standard assignment operators, _e.g._ `%<>%`
74
#' - objects used as conditions in `if` statements: `if (<condition>)`
75
#' - objects used to iterate over in `for` loops: `for(i in <sequence>)`
76
#' - creating and evaluating language objects, _e.g._ `eval(<call>)`
77
#'
78
#' @return
79
#' The code used in the `qenv` in the form specified by `deparse`.
80
#'
81
#' @examples
82
#' # retrieve code
83
#' q <- within(qenv(), {
84
#'   a <- 1
85
#'   b <- 2
86
#' })
87
#' get_code(q)
88
#' get_code(q, deparse = FALSE)
89
#' get_code(q, names = "a")
90
#'
91
#' q <- qenv()
92
#' q <- eval_code(q, code = c("a <- 1", "b <- 2"))
93
#' get_code(q, names = "a")
94
#'
95
#' @aliases get_code,qenv-method
96
#' @aliases get_code,qenv.error-method
97
#'
98
#' @export
99
setGeneric("get_code", function(object, deparse = TRUE, names = NULL, ...) {
100131x
  dev_suppress(object)
101131x
  standardGeneric("get_code")
102
})
103
104
setMethod("get_code", signature = "qenv", function(object, deparse = TRUE, names = NULL, ...) {
105129x
  checkmate::assert_flag(deparse)
106129x
  checkmate::assert_character(names, min.len = 1L, null.ok = TRUE)
107
108
  # Normalize in case special it is backticked
109129x
  if (!is.null(names)) {
11086x
    names <- gsub("^`(.*)`$", "\\1", names)
111
  }
112
113129x
  code <- if (!is.null(names)) {
11486x
    get_code_dependency(object@code, names, ...)
115
  } else {
11643x
    object@code
117
  }
118
119129x
  if (deparse) {
120127x
    paste(unlist(code), collapse = "\n")
121
  } else {
1222x
    parse(text = paste(c("{", unlist(code), "}"), collapse = "\n"), keep.source = TRUE)
123
  }
124
})
125
126
setMethod("get_code", signature = "qenv.error", function(object, ...) {
1272x
  stop(
1282x
    errorCondition(
1292x
      sprintf(
1302x
        "%s\n\ntrace: \n %s\n",
1312x
        conditionMessage(object),
1322x
        paste(object$trace, collapse = "\n ")
133
      ),
1342x
      class = c("validation", "try-error", "simpleError")
135
    )
136
  )
137
})
1
#' Display `qenv` object
2
#'
3
#' Prints the `qenv` object.
4
#'
5
#' @param object (`qenv`)
6
#'
7
#' @return `object`, invisibly.
8
#'
9
#' @examples
10
#' q <- qenv()
11
#' q1 <- eval_code(q, expression(a <- 5, b <- data.frame(x = 1:10)))
12
#' q1
13
#'
14
#' @aliases show-qenv
15
#'
16
#' @importFrom methods show
17
#' @export
18
setMethod("show", "qenv", function(object) {
19!
  env <- get_env(object)
20!
  header <- cli::col_blue(sprintf("<environment: %s>", rlang::env_label(env)))
21!
  parent <- sprintf("Parent: <environment: %s>", rlang::env_label(rlang::env_parent(env)))
22!
  cat(cli::style_bold(header), "\U1F512", "\n")
23!
  cat(parent, "\n")
24
25!
  shown <- ls(object)
26!
  if (length(shown > 0L)) cat(cli::style_bold("Bindings:\n"))
27!
  lapply(shown, function(x) {
28!
    cat(
29!
      sprintf(
30!
        "- %s: [%s]\n",
31!
        deparse(rlang::sym(x), backtick = TRUE),
32!
        class(object[[x]])[1]
33
      )
34
    )
35
  })
36
37!
  hidden <- setdiff(ls(object, all.names = TRUE), shown)
38!
  lapply(hidden, function(x) {
39!
    cat(
40!
      cli::style_blurred(
41!
        sprintf(
42!
          "- %s: [%s]\n",
43!
          deparse(rlang::sym(x), backtick = TRUE),
44!
          class(object[[x]])[1]
45
        )
46
      )
47
    )
48
  })
49
50!
  invisible(object)
51
})
1
#' Get messages from `qenv` object
2
#'
3
#' Retrieve all messages raised during code evaluation in a `qenv`.
4
#'
5
#' @param object (`qenv`)
6
#'
7
#' @return `character` containing warning information or `NULL` if no messages.
8
#'
9
#' @examples
10
#' data_q <- qenv()
11
#' data_q <- eval_code(data_q, "iris_data <- iris")
12
#' warning_qenv <- eval_code(
13
#'   data_q,
14
#'   bquote(p <- hist(iris_data[, .("Sepal.Length")], ff = ""))
15
#' )
16
#' cat(get_messages(warning_qenv))
17
#'
18
#' @name get_messages
19
#' @rdname get_messages
20
#' @aliases get_messages,qenv-method
21
#' @aliases get_messages,qenv.error-method
22
#' @aliases get_messages,NULL-method
23
#'
24
#' @export
25
setGeneric("get_messages", function(object) {
269x
  dev_suppress(object)
279x
  standardGeneric("get_messages")
28
})
29
30
setMethod("get_messages", signature = "qenv", function(object) {
317x
  get_warn_message_util(object, "message")
32
})
33
34
setMethod("get_messages", signature = "qenv.error", function(object) {
351x
  NULL
36
})
37
38
setMethod("get_messages", "NULL", function(object) {
391x
  NULL
40
})
1
#' Join `qenv` objects
2
#'
3
#' @description
4
#' `r lifecycle::badge("deprecated")`
5
#' Instead of [join()] use [c()].
6
#'
7
#' @param ... function is deprecated.
8
#'
9
#' @name join
10
#' @rdname join
11
#'
12
#' @export
13!
join <- function(...) lifecycle::deprecate_stop("0.7.0", "join()", "c()")
1
#' Concatenate two `qenv` objects
2
#'
3
#' Combine two `qenv` objects by simple concatenate their environments and the code.
4
#'
5
#' We recommend to use the `join` method to have a stricter control
6
#' in case `x` and `y` contain duplicated bindings and code.
7
#' RHS argument content has priority over the LHS one.
8
#'
9
#' @param x (`qenv`)
10
#' @param y (`qenv`)
11
#'
12
#' @return `qenv` object.
13
#'
14
#' @examples
15
#' q <- qenv()
16
#' q1 <- eval_code(q, expression(iris1 <- iris, mtcars1 <- mtcars))
17
#' q2 <- q1
18
#' q1 <- eval_code(q1, "iris2 <- iris")
19
#' q2 <- eval_code(q2, "mtcars2 <- mtcars")
20
#' qq <- concat(q1, q2)
21
#' get_code(qq)
22
#'
23
#' @include qenv-errors.R
24
#'
25
#' @name concat
26
#' @rdname concat
27
#' @aliases concat,qenv,qenv-method
28
#' @aliases concat,qenv.error,ANY-method
29
#' @aliases concat,qenv,qenv.error-method
30
#'
31
#' @export
329x
setGeneric("concat", function(x, y) standardGeneric("concat"))
33
34
setMethod("concat", signature = c("qenv", "qenv"), function(x, y) {
355x
  y@code <- c(x@code, y@code)
36
37
  # insert (and overwrite) objects from y to x
385x
  y@.xData <- rlang::env_clone(y@.xData, parent = parent.env(.GlobalEnv))
395x
  rlang::env_coalesce(env = y@.xData, from = x@.xData)
405x
  y
41
})
42
43
setMethod("concat", signature = c("qenv.error", "ANY"), function(x, y) {
443x
  x
45
})
46
47
setMethod("concat", signature = c("qenv", "qenv.error"), function(x, y) {
481x
  y
49
})
1
#' Evaluate code in `qenv`
2
#'
3
#' @details
4
#'
5
#' `eval_code()` evaluates given code in the `qenv` environment and appends it to the `code` slot.
6
#' Thus, if the `qenv` had been instantiated empty, contents of the environment are always a result of the stored code.
7
#'
8
#' @param object (`qenv`)
9
#' @param code (`character`, `language` or `expression`) code to evaluate.
10
#' It is possible to preserve original formatting of the `code` by providing a `character` or an
11
#' `expression` being a result of `parse(keep.source = TRUE)`.
12
#' @param ... ([`dots`]) additional arguments passed to future methods.
13
#'
14
#' @return
15
#' `qenv` environment with `code/expr` evaluated or `qenv.error` if evaluation fails.
16
#'
17
#' @examples
18
#' # evaluate code in qenv
19
#' q <- qenv()
20
#' q <- eval_code(q, "a <- 1")
21
#' q <- eval_code(q, "b <- 2L # with comment")
22
#' q <- eval_code(q, quote(library(checkmate)))
23
#' q <- eval_code(q, expression(assert_number(a)))
24
#'
25
#' @aliases eval_code,qenv-method
26
#' @aliases eval_code,qenv.error-method
27
#' @seealso [within.qenv]
28
#' @export
29227x
setGeneric("eval_code", function(object, code, ...) standardGeneric("eval_code"))
30
31
setMethod("eval_code", signature = c(object = "qenv"), function(object, code, ...) {
32227x
  if (!is.language(code) && !is.character(code)) {
333x
    stop("eval_code accepts code being language or character")
34
  }
35224x
  code <- .preprocess_code(code)
36
  # preprocess code to ensure it is a character vector
37224x
  .eval_code(object = object, code = code, ...)
38
})
39
40!
setMethod("eval_code", signature = c(object = "qenv.error"), function(object, code, ...) object)
41
42
#' @keywords internal
43
.eval_code <- function(object, code, ...) {
44224x
  if (identical(trimws(code), "") || length(code) == 0) {
452x
    return(object)
46
  }
47222x
  code <- paste(split_code(code), collapse = "\n")
48
49222x
  object@.xData <- rlang::env_clone(object@.xData, parent = parent.env(object@.xData))
50222x
  parsed_code <- parse(text = code, keep.source = TRUE)
51
52222x
  old <- evaluate::inject_funs(
53222x
    library = function(...) {
544x
      x <- library(...)
554x
      if (!identical(parent.env(object@.xData), parent.env(.GlobalEnv))) {
562x
        parent.env(object@.xData) <- parent.env(.GlobalEnv)
57
      }
584x
      invisible(x)
59
    }
60
  )
61222x
  out <- evaluate::evaluate(
62222x
    code,
63222x
    envir = object@.xData,
64222x
    stop_on_error = 1,
65222x
    output_handler = evaluate::new_output_handler(value = identity)
66
  )
67222x
  out <- evaluate::trim_intermediate_plots(out)
68
69222x
  evaluate::inject_funs(old) # remove library() override
70
71222x
  new_code <- list()
72222x
  for (this in out) {
73507x
    if (inherits(this, "source")) {
74431x
      this_code <- gsub("\n$", "", this$src)
75431x
      attr(this_code, "dependency") <- extract_dependency(parse(text = this_code, keep.source = TRUE))
76431x
      new_code <- c(new_code, stats::setNames(list(this_code), sample.int(.Machine$integer.max, size = 1)))
77
    } else {
7876x
      last_code <- new_code[[length(new_code)]]
7976x
      if (inherits(this, "error")) {
8014x
        return(
8114x
          errorCondition(
8214x
            message = sprintf(
8314x
              "%s \n when evaluating qenv code:\n%s",
8414x
              cli::ansi_strip(conditionMessage(this)),
8514x
              last_code
86
            ),
8714x
            class = c("qenv.error", "try-error", "simpleError"),
8814x
            trace = unlist(c(object@code, list(new_code)))
89
          )
90
        )
91
      }
9262x
      attr(last_code, "outputs") <- c(attr(last_code, "outputs"), list(this))
9362x
      new_code[[length(new_code)]] <- last_code
94
    }
95
  }
96
97208x
  object@code <- c(object@code, new_code)
98208x
  lockEnvironment(object@.xData, bindings = TRUE)
99208x
  object
100
}
101
102224x
setGeneric(".preprocess_code", function(code) standardGeneric(".preprocess_code"))
10371x
setMethod(".preprocess_code", signature = c("character"), function(code) paste(code, collapse = "\n"))
104
setMethod(".preprocess_code", signature = c("ANY"), function(code) {
105153x
  if (is.expression(code) && length(attr(code, "wholeSrcref"))) {
1062x
    paste(attr(code, "wholeSrcref"), collapse = "\n")
107
  } else {
108151x
    paste(
109151x
      vapply(lang2calls(code), deparse1, collapse = "\n", character(1L)),
110151x
      collapse = "\n"
111
    )
112
  }
113
})
1
#' Reproducible class with environment and code
2
#'
3
#' Reproducible class with environment and code.
4
#' @name qenv-class
5
#' @rdname qenv-class
6
#' @slot .xData (`environment`) environment with content was generated by the evaluation
7
#' @slot code (`named list` of `character`) representing code necessary to reproduce the environment.
8
#' Read more in Code section.
9
#'  of the `code` slot.
10
#'
11
#' @section Code:
12
#'
13
#' Each code element is a character representing one call. Each element is named with the random
14
#' identifier to make sure uniqueness when joining. Each element has possible attributes:
15
#' - `warnings` (`character`) the warnings output when evaluating the code element.
16
#' - `messages` (`character`) the messages output when evaluating the code element.
17
#' - `dependency` (`character`) names of objects that appear in this call and gets affected by this call,
18
#' separated by `<-` (objects on LHS of `<-` are affected by this line, and objects on RHS are affecting this line).
19
#'
20
#' @keywords internal
21
#' @exportClass qenv
22
setClass(
23
  "qenv",
24
  slots = c(code = "list"),
25
  contains = "environment"
26
)
27
28
#' It initializes the `qenv` class
29
#' @noRd
30
setMethod(
31
  "initialize",
32
  "qenv",
33
  function(.Object, .xData, code = list(), ...) { # nolint: object_name.
34208x
    parent <- parent.env(.GlobalEnv)
35208x
    new_xdata <- if (rlang::is_missing(.xData)) {
36206x
      new.env(parent = parent)
37
    } else {
382x
      checkmate::assert_environment(.xData)
391x
      rlang::env_clone(.xData, parent = parent)
40
    }
41207x
    lockEnvironment(new_xdata, bindings = TRUE)
42
43
    # .xData needs to be unnamed as the `.environment` constructor allows at
44
    # most 1 unnamed formal argument of class `environment`.
45
    # See methods::findMethods("initialize")$.environment
46207x
    methods::callNextMethod(
47207x
      .Object,
48207x
      new_xdata, # Mandatory use of unnamed environment arg
49207x
      code = code, ...
50
    )
51
  }
52
)
53
54
#' It takes a `qenv` class and returns `TRUE` if the input is valid
55
#' @name qenv-class
56
#' @keywords internal
57
setValidity("qenv", function(object) {
58
  if (any(duplicated(names(object@code)))) {
59
    "@code must have unique names."
60
  } else if (!environmentIsLocked(object@.xData)) {
61
    "@.xData must be locked."
62
  } else {
63
    TRUE
64
  }
65
})
1
# needed to handle try-error
2
setOldClass("qenv.error")
3
4
#' @export
5
as.list.qenv.error <- function(x, ...) {
6!
  stop(errorCondition(
7!
    list(message = conditionMessage(x)),
8!
    class = c("validation", "try-error", "simpleError")
9
  ))
10
}
1
#' If two `qenv` can be joined
2
#'
3
#' Checks if two `qenv` objects can be combined.
4
#' For more information, please see [`join`]
5
#' @param x (`qenv`)
6
#' @param y (`qenv`)
7
#' @return `TRUE` if able to join or `character` used to print error message.
8
#' @keywords internal
9
.check_joinable <- function(x, y) {
1016x
  checkmate::assert_class(x, "qenv")
1116x
  checkmate::assert_class(y, "qenv")
12
1316x
  common_names <- intersect(rlang::env_names(x@.xData), rlang::env_names(y@.xData))
1416x
  is_overwritten <- vapply(common_names, function(el) {
1513x
    !identical(get(el, x@.xData), get(el, y@.xData))
1616x
  }, logical(1))
1716x
  if (any(is_overwritten)) {
182x
    return(
192x
      paste(
202x
        "Not possible to join qenv objects if anything in their environment has been modified.\n",
212x
        "Following object(s) have been modified:\n - ",
222x
        paste(common_names[is_overwritten], collapse = "\n - ")
23
      )
24
    )
25
  }
26
2714x
  x_id <- names(x@code)
2814x
  y_id <- names(y@code)
29
3014x
  shared_ids <- intersect(x_id, y_id)
3114x
  if (length(shared_ids) == 0) {
328x
    return(TRUE)
33
  }
34
356x
  shared_in_x <- match(shared_ids, x_id)
366x
  shared_in_y <- match(shared_ids, y_id)
37
38
  # indices of shared ids should be 1:n in both slots
396x
  if (identical(shared_in_x, shared_in_y) && identical(shared_in_x, seq_along(shared_ids))) {
404x
    TRUE
412x
  } else if (!identical(shared_in_x, shared_in_y)) {
421x
    paste(
431x
      "The common shared code of the qenvs does not occur in the same position in both qenv objects",
441x
      "so they cannot be joined together as it's impossible to determine the evaluation's order.",
451x
      collapse = ""
46
    )
47
  } else {
481x
    paste(
491x
      "There is code in the qenv objects before their common shared code",
501x
      "which means these objects cannot be joined.",
511x
      collapse = ""
52
    )
53
  }
54
}
55
56
#' @rdname join
57
#' @param ... (`qenv` or `qenv.error`).
58
#' @examples
59
#' q <- qenv()
60
#' q1 <- within(q, {
61
#'   iris1 <- iris
62
#'   mtcars1 <- mtcars
63
#' })
64
#' q1 <- within(q1, iris2 <- iris)
65
#' q2 <- within(q1, mtcars2 <- mtcars)
66
#' qq <- c(q1, q2)
67
#' cat(get_code(qq))
68
#'
69
#' @export
70
c.qenv <- function(...) {
71224x
  dots <- rlang::list2(...)
72224x
  if (!checkmate::test_list(dots[-1], types = c("qenv", "qenv.error"))) {
73207x
    return(NextMethod(c, dots[[1]]))
74
  }
75
7617x
  first_non_qenv_ix <- which.min(vapply(dots, inherits, what = "qenv", logical(1)))
7717x
  if (first_non_qenv_ix > 1) {
781x
    return(dots[[first_non_qenv_ix]])
79
  }
80
8116x
  Reduce(
8216x
    x = dots[-1],
8316x
    init = dots[[1]],
8416x
    f = function(x, y) {
8516x
      join_validation <- .check_joinable(x, y)
86
87
      # join expressions
8816x
      if (!isTRUE(join_validation)) {
894x
        stop(join_validation)
90
      }
91
9212x
      x@code <- utils::modifyList(x@code, y@code)
93
94
      # insert (and overwrite) objects from y to x
9512x
      x@.xData <- rlang::env_clone(x@.xData, parent = parent.env(.GlobalEnv))
9612x
      rlang::env_coalesce(env = x@.xData, from = y@.xData)
9712x
      x
98
    }
99
  )
100
}
101
102
#' @rdname join
103
#' @export
104
c.qenv.error <- function(...) {
1053x
  rlang::list2(...)[[1]]
106
}
1
#' Access environment included in `qenv`
2
#'
3
#' The access of environment included in the `qenv` that contains all data objects.
4
#'
5
#' @param object (`qenv`).
6
#'
7
#' @return An `environment` stored in `qenv` with all data objects.
8
#'
9
#' @examples
10
#' q <- qenv()
11
#' q1 <- within(q, {
12
#'   a <- 5
13
#'   b <- data.frame(x = 1:10)
14
#' })
15
#' get_env(q1)
16
#'
17
#' @aliases get_env,qenv-method
18
#' @aliases get_env,qenv.error-method
19
#'
20
#' @export
21
setGeneric("get_env", function(object) {
2214x
  standardGeneric("get_env")
23
})
24
2514x
setMethod("get_env", "qenv", function(object) object@.xData)
26
27!
setMethod("get_env", "qenv.error", function(object) object)
1
#' Subsets `qenv`
2
#'
3
#' @description
4
#' Subsets [`qenv`] environment and limits the code to the necessary needed to build limited objects.
5
#'
6
#' @param x (`qenv`)
7
#' @param names (`character`) names of objects included in [`qenv`] to subset. Names not present in [`qenv`]
8
#' are skipped.
9
#' @param ... internal usage, please ignore.
10
#'
11
#' @name subset-qenv
12
#'
13
#' @examples
14
#' q <- qenv()
15
#' q <- eval_code(q, "a <- 1;b<-2")
16
#' q["a"]
17
#' q[c("a", "b")]
18
#'
19
#' @export
20
`[.qenv` <- function(x, names, ...) {
2112x
  checkmate::assert_character(names, any.missing = FALSE)
2212x
  possible_names <- ls(get_env(x), all.names = TRUE)
2312x
  names_corrected <- intersect(names, possible_names)
2412x
  env <- if (length(names_corrected)) {
259x
    names_missing <- setdiff(names, possible_names)
269x
    if (length(names_missing)) {
272x
      warning(
282x
        sprintf(
292x
          "Some 'names' do not exist in the environment of the '%s'. Skipping those: %s.",
302x
          class(x)[1],
312x
          paste(names_missing, collapse = ", ")
32
        )
33
      )
34
    }
359x
    list2env(as.list(x, all.names = TRUE)[names_corrected], parent = parent.env(.GlobalEnv))
36
  } else {
373x
    warning(
383x
      sprintf(
393x
        "None of 'names' exist in the environment of the '%1$s'. Returning empty '%1$s'.",
403x
        class(x)[1]
41
      ),
423x
      call. = FALSE
43
    )
443x
    new.env(parent = parent.env(.GlobalEnv))
45
  }
4612x
  lockEnvironment(env)
4712x
  x@.xData <- env
48
4912x
  normalized_names <- gsub("^`(.*)`$", "\\1", names)
5012x
  x@code <- get_code_dependency(x@code, names = normalized_names, ...)
51
5212x
  x
53
}
1
#' Get object from `qenv`
2
#'
3
#' @description
4
#' `r lifecycle::badge("deprecated")`
5
#' Instead of [get_var()] use native \R operators/functions:
6
#' `x[[name]]`, `x$name` or [get()]:
7
#'
8
#' @param ... function is deprecated.
9
#' @param x (`qenv`)
10
#' @param i (`character(1)`) variable name.
11
#'
12
#' @export
13!
get_var <- function(...) lifecycle::deprecate_stop("0.7.0", "get_var()", "base::get()")
14
15
#' @rdname get_var
16
#' @export
17
`[[.qenv.error` <- function(x, i) {
181x
  stop(errorCondition(
191x
    list(message = conditionMessage(x)),
201x
    class = c("validation", "try-error", "simpleError")
21
  ))
22
}
23
24
#' @export
254x
names.qenv.error <- function(x) NULL
26
27
#' @export
28
`$.qenv.error` <- function(x, name) {
29
  # Must allow access of elements in qenv.error object (message, call, trace, ...)
30
  # Otherwise, it will enter an infinite recursion with the `conditionMessage(x)` call.
318x
  if (exists(name, x)) {
327x
    return(NextMethod("$", x))
33
  }
34
351x
  class(x) <- setdiff(class(x), "qenv.error")
361x
  stop(errorCondition(
371x
    list(message = conditionMessage(x)),
381x
    class = c("validation", "try-error", "simpleError")
39
  ))
40
}
1
#' @export
2!
length.qenv <- function(x) length(x@.xData)
3
4
#' @export
520x
length.qenv.error <- function(x) 0
1
#' Instantiates a `qenv` environment
2
#'
3
#' @description
4
#' `r badge("stable")`
5
#'
6
#' Instantiates a `qenv` environment.
7
#'
8
#' @details
9
#' `qenv` class has following characteristics:
10
#'
11
#' - It inherits from the environment and methods such as [`$`], [get()], [ls()], [as.list()],
12
#' [parent.env()] work out of the box.
13
#' - `qenv` is a locked environment, and data modification is only possible through the [eval_code()]
14
#'   and [within.qenv()] functions.
15
#' - It stores metadata about the code used to create the data (see [get_code()]).
16
#' - It supports slicing (see [`subset-qenv`])
17
#' - It is immutable which means that each code evaluation does not modify the original `qenv`
18
#'   environment directly. See the following code:
19
#'
20
#'   ```
21
#'   q1 <- qenv()
22
#'   q2 <- eval_code(q1, "a <- 1")
23
#'   identical(q1, q2) # FALSE
24
#'   ```
25
#'
26
#' @name qenv
27
#'
28
#' @return `qenv` environment.
29
#'
30
#' @seealso [eval_code()], [get_var()], [`subset-qenv`], [get_env()],[get_warnings()], [join()], [concat()]
31
#' @examples
32
#' q <- qenv()
33
#' q2 <- within(q, a <- 1)
34
#' ls(q2)
35
#' q2$a
36
#' @export
37
qenv <- function() {
38205x
  methods::new("qenv")
39
}
1
#' Get warnings from `qenv` object
2
#'
3
#' Retrieve all warnings raised during code evaluation in a `qenv`.
4
#'
5
#' @param object (`qenv`)
6
#'
7
#' @return `character` containing warning information or `NULL` if no warnings.
8
#'
9
#' @examples
10
#' data_q <- qenv()
11
#' data_q <- eval_code(data_q, "iris_data <- iris")
12
#' warning_qenv <- eval_code(
13
#'   data_q,
14
#'   bquote(p <- hist(iris_data[, .("Sepal.Length")], ff = ""))
15
#' )
16
#' cat(get_warnings(warning_qenv))
17
#'
18
#' @name get_warnings
19
#' @rdname get_warnings
20
#' @aliases get_warnings,qenv-method
21
#' @aliases get_warnings,qenv.error-method
22
#' @aliases get_warnings,NULL-method
23
#'
24
#' @export
25
setGeneric("get_warnings", function(object) {
269x
  dev_suppress(object)
279x
  standardGeneric("get_warnings")
28
})
29
30
setMethod("get_warnings", signature = "qenv", function(object) {
317x
  get_warn_message_util(object, "warning")
32
})
33
34
setMethod("get_warnings", signature = "qenv.error", function(object) {
351x
  NULL
36
})
37
38
setMethod("get_warnings", "NULL", function(object) {
391x
  NULL
40
})
1
#' Evaluate code in `qenv`
2
#' @details
3
#' `within()` is a convenience method that wraps `eval_code` to provide a simplified way of passing expression.
4
#' `within` accepts only inline expressions (both simple and compound) and allows to substitute `expr`
5
#' with `...` named argument values.
6
#' Functions that trigger side effects like `options` or `set.seed` can be linked to specific objects for further code retrieval (with `get_code`), but only through `eval_code` where code input as `character`. `within` works on `expressions` that do not preserve comments, hence you can not use `# @linksto` tag explained in `get_code`.
7
#' @alias within
8
#' @section Using language objects with `within`:
9
#' Passing language objects to `expr` is generally not intended but can be achieved with `do.call`.
10
#' Only single `expression`s will work and substitution is not available. See examples.
11
#'
12
#' @param data (`qenv`)
13
#' @param expr (`expression`) to evaluate. Must be inline code, see `Using language objects...`
14
#' @param ... named argument value will substitute a symbol in the `expr` matched by the name.
15
#' For practical usage see Examples section below.
16
#'
17
#' @examples
18
#' # evaluate code using within
19
#' q <- qenv()
20
#' q <- within(q, {
21
#'   i <- iris
22
#' })
23
#' q <- within(q, {
24
#'   m <- mtcars
25
#'   f <- faithful
26
#' })
27
#' q
28
#' get_code(q)
29
#'
30
#' # inject values into code
31
#' q <- qenv()
32
#' q <- within(q, i <- iris)
33
#' within(q, print(dim(subset(i, Species == "virginica"))))
34
#' within(q, print(dim(subset(i, Species == species)))) # fails
35
#' within(q, print(dim(subset(i, Species == species))), species = "versicolor")
36
#' species_external <- "versicolor"
37
#' within(q, print(dim(subset(i, Species == species))), species = species_external)
38
#'
39
#' # pass language objects
40
#' expr <- expression(i <- iris, m <- mtcars)
41
#' within(q, expr) # fails
42
#' do.call(within, list(q, expr))
43
#'
44
#' exprlist <- list(expression(i <- iris), expression(m <- mtcars))
45
#' within(q, exprlist) # fails
46
#' do.call(within, list(q, do.call(c, exprlist)))
47
#'
48
#' @export
49
#'
50
within.qenv <- function(data, expr, ...) {
5164x
  expr <- as.expression(substitute(expr))
5264x
  extras <- list(...)
53
54
  # Inject extra values into expressions.
5564x
  calls <- lapply(expr, function(x) do.call(substitute, list(x, env = extras)))
5664x
  do.call(
5764x
    eval_code,
5864x
    utils::modifyList(extras, list(object = data, code = as.expression(calls)))
59
  )
60
}
61
62
63
#' @keywords internal
64
#'
65
#' @export
66
within.qenv.error <- function(data, expr, ...) {
671x
  data
68
}
1
#' Get outputs
2
#'
3
#' @description `r lifecycle::badge("experimental")`
4
#'
5
#' `eval_code` evaluates code silently so plots and prints don't show up in the console or graphic devices.
6
#' If one wants to use an output outside of the `qenv` (e.g. use a graph in `renderPlot`) then use `get_outputs`.
7
#' @param object (`qenv`)
8
#' @return list of outputs generated in a `qenv``
9
#' @examples
10
#' q <- eval_code(
11
#'   qenv(),
12
#'   quote({
13
#'     a <- 1
14
#'     print("I'm an output")
15
#'     plot(1)
16
#'   })
17
#' )
18
#' get_outputs(q)
19
#'
20
#' @aliases get_outputs,qenv-method
21
#'
22
#' @export
2316x
setGeneric("get_outputs", function(object) standardGeneric("get_outputs"))
24
25
setMethod("get_outputs", signature = "qenv", function(object) {
2616x
  Reduce(
2716x
    function(x, y) c(x, attr(y, "outputs")),
2816x
    init = list(),
2916x
    x = object@code
30
  )
31
})

[8]ページ先頭

©2009-2025 Movatter.jp