.
5 |
6 |
7 | # autoimport 0.1.1
8 |
9 | - Submission to CRAN
10 |
11 | # autoimport 0.1.0
12 |
13 | First stable version.
14 |
15 | - Added option to centralize imports in the package-level documentation.
16 | - Package-prefixed function calls are now ignored by default. Set `options(ignore_prefixed=FALSE)` to import them back.
17 | - Ignore any line using the `#autoimport_ignore` comment.
18 | - Implement comments in inst/IMPORTLIST
19 |
20 | # autoimport 0.0.1
21 |
22 | - Draft version
23 |
--------------------------------------------------------------------------------
/R/ai_ask.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | #' Take a dataframe from `autoimport_parse()`, finds the functions for
4 | #' which the source package is uncertain, and asks the user interactively
5 | #' about them.
6 | #' Returns the input dataframe, with column `pkg` being a single-value character
7 | #'
8 | #' @importFrom cli cli_h1 cli_inform
9 | #' @importFrom dplyr distinct filter left_join mutate pull rowwise ungroup
10 | #' @importFrom purrr map2_chr
11 | #' @noRd
12 | autoimport_ask = function(data_imports, ns, importlist_path, verbose){
13 | pref_importlist = get_importlist(importlist_path)
14 | unsure_funs = data_imports %>%
15 | filter(action=="ask_user") %>%
16 | distinct(fun, pkg) %>%
17 | left_join(pref_importlist, by="fun")
18 |
19 | defined_funs = unsure_funs %>%
20 | filter(!is.na(pref_pkg))
21 | undefined_funs = unsure_funs %>%
22 | filter(is.na(pref_pkg))
23 |
24 | if(verbose>0 && nrow(unsure_funs)>0){
25 | cli_h1("Attributing")
26 | }
27 |
28 | if(verbose>0 && nrow(defined_funs)>0){
29 | cli_inform(c(i="Automatically attributing {nrow(defined_funs)} function import{?s}
30 | as predefined in {.file {importlist_path}}"))
31 | }
32 |
33 | if(nrow(undefined_funs)>0){
34 | unsure_funs = unsure_funs %>%
35 | rowwise() %>%
36 | mutate(
37 | defined_in_importlist = !is.na(pref_pkg),
38 | pref_pkg = ifelse(defined_in_importlist, pref_pkg,
39 | user_input_1package(fun, pkg, ns))
40 | ) %>%
41 | ungroup()
42 |
43 | ask_update_importlist(unsure_funs, importlist_path, verbose)
44 | }
45 |
46 | fun_replace_list = unsure_funs %>% pull(pref_pkg, name=fun) %>% as.list()
47 |
48 | data_imports %>%
49 | mutate(
50 | pkg = map2_chr(pkg, fun, ~ifelse(length(.x)>1, fun_replace_list[[.y]], .x))
51 | )
52 | }
53 |
54 | #' @importFrom glue glue
55 | #' @importFrom utils menu
56 | #' @noRd
57 | user_input_pkg_choose = function(unsure_funs){
58 | title = glue("\n\nThere are {nrow(unsure_funs)} functions that can be imported from several packages. What do you want to do?")
59 | choices = c("Choose the package for each", "Choose for me please", "Abort mission")
60 | menu(choices=choices, title=title)
61 | }
62 |
63 | #' @importFrom glue glue
64 | #' @importFrom purrr map_int
65 | #' @importFrom stringr str_pad
66 | #' @importFrom utils menu
67 | #' @noRd
68 | user_input_1package = function(fun, pkg, ns){
69 | ni = map_int(pkg, ~sum(ns$importFrom$from==.x))
70 | pkg = pkg[order(ni, decreasing=TRUE)]
71 | ni = ni[order(ni, decreasing=TRUE)]
72 | select_first = getOption("autoimport_testing_dont_ask_select_first", FALSE)
73 | if(select_first) return(pkg[1])
74 | label = glue(" ({n} function{s} imported)", n=str_pad(ni, max(nchar(ni))), s = ifelse(ni>1, "s", ""))
75 | label[pkg=="base"] = ""
76 | title = glue("`{fun}()` can be found in several packages.\n From which one do you want to import it:")
77 | choices = glue("{pkg}{label}")
78 | i = menu(choices=choices, title=title)
79 | if(i==0) return(NA)
80 | pkg[i]
81 | }
82 |
83 |
84 | #' @importFrom cli cli_inform
85 | #' @importFrom dplyr filter
86 | #' @importFrom glue glue
87 | #' @importFrom utils menu
88 | ask_update_importlist = function(user_asked, path="inst/IMPORTLIST", verbose=TRUE){
89 | user_asked = user_asked %>% filter(!defined_in_importlist)
90 | resp = getOption("autoimport_testing_ask_save_importlist")
91 | if(!is.null(resp)){
92 | stopifnot(resp==1 || resp==2)
93 | x = if(resp==1) "" else "not "
94 | if(verbose>0) cli_inform(c(i="TESTING: {x}saving choices in {.file {path}}"))
95 | } else {
96 | s = if(nrow(user_asked)>1) "s" else ""
97 | title = glue("\n\nDo you want to save your choices about these {nrow(user_asked)} function{s} in `{path}`?")
98 | choices = c("Yes", "No")
99 | resp = menu(choices=choices, title=title)
100 | }
101 |
102 | if(resp==1){
103 | update_importlist(user_asked, path)
104 | }
105 | invisible(NULL)
106 | }
107 |
--------------------------------------------------------------------------------
/R/ai_parse.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | #' Take a list of source references (`srcref`, one per function) and parse them:
4 | #' - what functions are called inside the function
5 | #' - what package they are most likely originated from
6 | #' Returns a dataframe with columns: file, source_fun, fun, pkg, action
7 | #' Uses a rds cache system at file and ref level
8 | #'
9 | #' @importFrom cli cli_h1 cli_inform
10 | #' @importFrom dplyr as_tibble
11 | #' @importFrom fs dir_create file_exists path_dir
12 | #' @importFrom purrr imap list_rbind map map_dbl map_depth
13 | #' @importFrom rlang hash hash_file
14 | #' @noRd
15 | #' @keywords internal
16 | autoimport_parse = function(ref_list, cache_path, use_cache, pkg_name, ns,
17 | deps, verbose) {
18 |
19 | if(verbose>0) cli_h1("Parsing")
20 |
21 | cache = if(file_exists(cache_path)) readRDS(cache_path) else list()
22 | read_from_cache = "read" %in% use_cache && !is.null(cache)
23 |
24 | import_list = ref_list %>%
25 | imap(function(refs, filename) {
26 | file_hash = hash_file(filename)
27 | filename = basename(filename)
28 | cache_file = cache[[filename]]
29 | cache_file_hash = if(is.null(cache_file[["..file_hash"]])) "" else cache_file[["..file_hash"]]
30 | if(isTRUE(read_from_cache) && file_hash==cache_file_hash){
31 | if(verbose>1) cli_inform(c(">"="Reading file {.file {filename}} (from cache)"))
32 | rtn_file = cache[[filename]][["..imports"]] %>%
33 | map(~{
34 | if(nrow(.x)>0) .x$ai_source = "cache_file"
35 | .x
36 | })
37 | } else {
38 | if(verbose>1) cli_inform(c(">"="Reading file {.file {filename}} (from file)"))
39 | rtn_file = refs %>%
40 | imap(function(ref, fun_name){
41 | cache_ref = cache_file[[fun_name]]
42 | cache_ref_hash = cache_ref[["ref_hash"]]
43 | if(length(cache_ref_hash)==0) cache_ref_hash=""
44 | ref_hash = hash(as.character(ref))
45 | if(isTRUE(read_from_cache) && ref_hash==cache_ref_hash) {
46 | rtn_ref = cache_ref[["imports"]]
47 | if(nrow(rtn_ref)>0) rtn_ref$ai_source = "cache_ref"
48 | } else {
49 | rtn_ref = parse_function(ref, fun_name, pkg_name=pkg_name,
50 | ns=ns, deps=deps, verbose=verbose)
51 | cache[[filename]][[fun_name]][["imports"]] <<- rtn_ref
52 | cache[[filename]][[fun_name]][["ref_hash"]] <<- ref_hash
53 | if(!is.null(rtn_ref) & nrow(rtn_ref)>0) rtn_ref$ai_source = "file"
54 | }
55 | rtn_ref
56 | })
57 | cache[[filename]][["..file_hash"]] <<- file_hash
58 | cache[[filename]][["..imports"]] <<- rtn_file
59 | }
60 | if(verbose>1){
61 | s = rtn_file %>% map_dbl(nrow) %>% sum()
62 | cli_inform(c("i"="Found {s} function{?s} to import in {length(rtn_file)}
63 | function{?s} or code chunk{?s}."))
64 | }
65 | rtn_file
66 | })
67 |
68 | if("write" %in% use_cache){
69 | dir_create(path_dir(cache_path))
70 | saveRDS(cache, file=cache_path)
71 | }
72 |
73 | n_imports = import_list %>% map_depth(2, nrow) %>% unlist() %>% sum()
74 | if(verbose>0) cli_inform(c(v="Found a total of {n_imports} potential function{?s} to import"))
75 |
76 | data_imports = import_list %>%
77 | map(~list_rbind(.x, names_to="source_fun")) %>%
78 | list_rbind(names_to="file") %>%
79 | as_tibble()
80 |
81 | warn_not_in_desc(data_imports, verbose)
82 | warn_not_found(data_imports, verbose)
83 |
84 | data_imports
85 | }
86 |
87 |
88 | # Utils ---------------------------------------------------------------------------------------
89 |
90 |
91 | #' used in [list_importFrom()], calls [parse_ref()]
92 | #' @importFrom cli cli_abort cli_inform
93 | #' @importFrom dplyr arrange filter mutate pull
94 | #' @importFrom glue glue
95 | #' @importFrom purrr imap list_rbind map_chr
96 | #' @importFrom stringr str_starts
97 | #' @importFrom tibble tibble
98 | #' @noRd
99 | #' @keywords internal
100 | parse_function = function(ref, fun_name, pkg_name, ns, deps, verbose){
101 | empty_ref = structure(list(fun = character(0), pkg = list(), pkg_str = character(0),
102 | action = character(0), reason = character(0), pkgs = list()),
103 | row.names = integer(0), class = "data.frame")
104 | loc = parse_ref(ref, pkg_name, ns, deps)
105 | if(verbose){
106 | if(str_starts(fun_name, "unnamed_")){
107 | cli_inform(c(i="Parsing code block {.code {fun_name}}"))
108 | } else {
109 | cli_inform(c(i="Parsing function {.fun {fun_name}}"))
110 | }
111 | }
112 | if(is.null(loc)) return(empty_ref)
113 | if(nrow(loc)==0) return(loc)
114 |
115 | rslt = loc %>%
116 | split(.$fun) %>%
117 | imap(~{
118 | rtn = list(.x$pkg)
119 | action = "nothing"
120 | # if(.y=="ggplot") browser()
121 |
122 | if(nrow(.x)==1) {
123 | if(isTRUE(.x$fun_is_inner)) {
124 | reason = glue("`{.y}()` is declared inside `fun()`.")
125 | } else if(is.na(.x$pkg)) {
126 | action = "warn"
127 | reason = glue("`{.y}()` not found in any loaded package.")
128 | } else if(.x$pkg==pkg_name) {
129 | reason = glue("`{.x$fun}()` is internal to {pkg_name}")
130 | } else if(.x$pkg=="base") {
131 | reason = glue("`{.x$fun}()` is base R")
132 | } else if(isTRUE(.x$fun_already_imported)) {
133 | reason = glue("`{.x$label}()` is unique and already imported.")
134 | } else if(isFALSE(.x$pkg_in_desc)) {
135 | action = "add_description"
136 | reason = glue("`{.y}()` only found in package `{.x$pkg}`,
137 | not found in DESCRIPTION.")
138 | } else {
139 | action = "add_pkg"
140 | reason = glue("`{.y}()` only found in package `{.x$pkg}`.")
141 | }
142 |
143 | } else {
144 | imported = .x %>% filter(fun_already_imported)
145 | base = .x %>% filter(pkg=="base")
146 |
147 | if(nrow(imported)>1) {
148 | dups = .x %>% filter(fun_already_imported) %>% pull(label)
149 | #should never happen, `autoimport_namespace_dup_error` first
150 | cli_abort(c("There are duplicates in NAMESPACE.", i="Functions: {.fun {dups}}"),
151 | .internal=TRUE)
152 | } else if(nrow(imported)==1){
153 | rtn = list(imported$pkg)
154 | reason = glue("`{imported$label}()` already imported.")
155 | } else {
156 | action = "ask_user"
157 | reason = "Multiple choices"
158 | }
159 | }
160 | tibble(fun=.y, pkg=rtn, action=action, reason=reason, pkgs=list(.x))
161 | }) %>%
162 | list_rbind() %>%
163 | mutate(pkg_str = map_chr(pkg, paste, collapse="/"), .after=pkg) %>%
164 | arrange(action)
165 |
166 | rslt
167 | }
168 |
169 |
170 | #' Used in [parse_function()], calls [get_function_source]
171 | #'
172 | #' @param ref a ref
173 | #' @param pkg_name package name (character)
174 | #' @param ns result of `parse_namespace()`
175 | #' @importFrom dplyr arrange bind_rows desc filter lag lead mutate pull select setdiff starts_with
176 | #' @importFrom purrr map map_int
177 | #' @importFrom rlang set_names
178 | #' @importFrom stringr str_detect str_subset
179 | #' @importFrom utils getParseData
180 | #' @noRd
181 | #' @keywords internal
182 | parse_ref = function(ref, pkg_name, ns, deps){
183 | ignore = "#.*autoimport_ignore"
184 | ref_chr = as.character(ref, useSource=TRUE) %>%
185 | str_subset(ignore, negate=TRUE)
186 |
187 | .fun = paste(ref_chr, collapse="\n")
188 |
189 | pd = getParseData(parse(text=.fun, keep.source=TRUE))
190 | non_comment = pd %>% filter(token!="COMMENT") %>% pull(text) %>% paste(collapse="")
191 | nms = pd$text[pd$token == "SYMBOL_FUNCTION_CALL"] %>% unique()
192 | nms = setdiff(nms, "autoimport")
193 |
194 | inner_vars = pd %>%
195 | filter(token!="expr") %>%
196 | filter(str_detect(lead(token), "ASSIGN") & token=="SYMBOL") %>%
197 | pull(text)
198 |
199 | if(getOption("autoimport_ignore_prefixed", TRUE)){
200 | nms_prefixed = pd$token == "SYMBOL_FUNCTION_CALL" & lag(pd$token, n=2)=="SYMBOL_PACKAGE"
201 | nms_prefixed = pd$text[nms_prefixed]
202 | nms = setdiff(nms, nms_prefixed)
203 | }
204 | if(getOption("autoimport_ignore_R6", TRUE)){
205 | nms_R6 = pd$token == "SYMBOL_FUNCTION_CALL" & lag(pd$token, n=1)=="'$'"
206 | nms_R6 = pd$text[nms_R6]
207 | nms = setdiff(nms, nms_R6)
208 | }
209 |
210 | if(length(nms)==0) return(NULL)
211 | loc = nms %>%
212 | set_names() %>%
213 | map(~get_function_source(fun=.x, pkg=pkg, ns=ns, pkg_name=pkg_name)) %>%
214 | bind_rows() %>%
215 | arrange(fun) %>%
216 | mutate(
217 | fun_is_inner = fun %in% inner_vars,
218 | pkg = ifelse(fun_is_inner, "inner", pkg),
219 | label = ifelse(is.na(pkg), NA, paste(pkg, fun, sep="::")),
220 | pkg_in_desc = pkg %in% deps$package,
221 | pkg_n_imports = map_int(pkg, ~sum(ns$importFrom$from==.x)),
222 | fun_is_private = pkg==pkg_name,
223 | fun_is_base = pkg %in% get_base_packages()
224 | ) %>%
225 | select(fun, pkg, label, starts_with("pkg_"), starts_with("fun_")) %>%
226 | arrange(fun,
227 | desc(fun_is_inner),
228 | desc(fun_is_private),
229 | desc(fun_already_imported),
230 | desc(pkg_in_desc),
231 | desc(pkg_n_imports),
232 | fun_is_base) #base packages last
233 | loc
234 | }
235 |
236 |
237 | #' used in [parse_ref()]
238 | #' get function source, with prioritizing known source if
239 | #' function is already imported or if it is private to the
240 | #' tested package
241 | #' @importFrom cli cli_abort
242 | #' @importFrom tibble tibble
243 | #' @noRd
244 | #' @keywords internal
245 | get_function_source = function(fun, pkg, ns, pkg_name){
246 | # if(fun=="abort") browser()
247 | pkg = get_anywhere(fun, add_pkgs=unique(ns$importFrom$from))
248 | already_imported = ns$importFrom$what==fun
249 | is_private = is_exported(fun, pkg=pkg_name, type=":::")
250 | if(isTRUE(is_private)) {
251 | pkg = pkg_name
252 | }
253 | if(any(already_imported)) {
254 | pkg = ns$importFrom$from[already_imported]
255 | }
256 | if(length(pkg)==0) {
257 | pkg = NA
258 | }
259 | if(isTRUE(is_private) && any(already_imported)){
260 | cli_abort("Function {.fn {fun}} is both imported from {.pkg {pkg}} in
261 | NAMESPACE and declared as a private function in {.pkg {pkg_name}}.",
262 | class="autoimport_conflict_import_private_error",
263 | call=main_caller$env)
264 | }
265 |
266 | tibble(fun=fun, pkg=pkg, fun_is_private=is_private, fun_already_imported=FALSE)
267 | }
268 |
269 |
270 | #' used in [autoimport_parse()]
271 | #' @importFrom cli cli_h2 cli_warn format_inline
272 | #' @importFrom dplyr filter pull summarise transmute
273 | #' @importFrom rlang set_names
274 | #' @noRd
275 | #' @keywords internal
276 | warn_not_found = function(data_imports, verbose){
277 | apply_basename = getOption("autoimport_warnings_files_basename", FALSE)
278 | not_found = data_imports %>%
279 | #filter(map_lgl(pkg, ~any(is.na(.x))))
280 | filter(is.na(pkg)) %>%
281 | transmute(fun, file=ifelse(apply_basename, basename(file), file))
282 |
283 | if(nrow(not_found)>0){
284 | if(verbose>0) cli_h2("Warning - Not found")
285 | txt = "{qty(fun)}Function{?s} {.fn {fun}} (in {.file {unique(file)}})"
286 | i = not_found %>%
287 | summarise(label = format_inline(txt),
288 | .by=file) %>%
289 | pull(label) %>%
290 | set_names("i")
291 | cli_warn(c("Functions not found:", i),
292 | class="autoimport_fun_not_found_warn")
293 | }
294 | invisible(TRUE)
295 | }
296 |
297 |
298 | #' @importFrom cli cli_h2 cli_warn
299 | #' @importFrom dplyr distinct filter transmute
300 | #' @importFrom glue glue
301 | #' @importFrom rlang set_names
302 | warn_not_in_desc = function(data_imports, verbose){
303 | apply_basename = getOption("autoimport_warnings_files_basename", FALSE)
304 | not_in_desc = data_imports %>%
305 | filter(action=="add_description") %>%
306 | transmute(file = ifelse(apply_basename, basename(file), file),
307 | source_fun, fun, pkg=pkg_str, action,
308 | label=glue("`{pkg}::{fun}()` in {file}")) %>%
309 | distinct(label)
310 |
311 | if(nrow(not_in_desc)>0){
312 | if(verbose>0) cli_h2("Warning - Not in DESCRIPTION")
313 | b = not_in_desc$label %>% as.character() %>% set_names(">")
314 | cli_warn(c("Importing functions not listed in the Imports section of DESCRIPTION:",
315 | b),
316 | class="autoimport_fun_not_in_desc_warn")
317 | }
318 | invisible(TRUE)
319 | }
320 |
--------------------------------------------------------------------------------
/R/ai_read.R:
--------------------------------------------------------------------------------
1 |
2 | #' Read a list of lines from `readr::read_lines()` (one per file)
3 | #' Returns a list of source references (`srcref`, one per function)
4 | #' See [base::srcfile()] for all methods and functions
5 | #'
6 | #' @importFrom cli cli_h1 cli_inform
7 | #' @importFrom purrr imap
8 | #' @importFrom utils getSrcref
9 | #' @noRd
10 | #' @keywords internal
11 | autoimport_read = function(lines_list, verbose) {
12 | if(verbose>0) cli_h1("Reading")
13 |
14 | ref_list = lines_list %>%
15 | imap(function(lines, file){
16 | parsed = parse(text=lines, keep.source=TRUE)
17 | comments_refs = getSrcref(parsed) %>% comments() %>% set_names_ref()
18 | if(verbose>1) cli_inform(c(i="Found {length(comments_refs)} function{?s} in
19 | file {.file {file}} ({length(lines)} lines)"))
20 | comments_refs
21 | })
22 | tot_lines = sum(lengths(lines_list))
23 | tot_refs = sum(lengths(ref_list))
24 | if(verbose>0) cli_inform(c(v="Found a total of {tot_refs} internal functions
25 | in {length(lines_list)} files ({tot_lines} lines)."))
26 |
27 | warn_duplicated(ref_list, verbose)
28 | ref_list
29 | }
30 |
31 |
32 | # Utils ---------------------------------------------------------------------------------------
33 |
34 |
35 | #' @importFrom cli cli_h2 cli_warn
36 | #' @importFrom dplyr arrange filter mutate rename
37 | #' @importFrom glue glue_data
38 | #' @importFrom purrr map
39 | #' @importFrom stringr str_detect
40 | #' @importFrom utils capture.output stack
41 | #' @noRd
42 | #' @keywords internal
43 | warn_duplicated = function(ref_list, verbose) {
44 | ref_list %>% map(~map(.x, ~attr(.x, "lines")))
45 | if(length(ref_list)==0) return(FALSE)
46 | dups = ref_list %>%
47 | map(~{
48 | lines = map(.x, ~attr(.x, "lines"))
49 | tibble(fun=names(.x), first_line=map_dbl(lines, 1), last_line=map_dbl(lines, 2))
50 | }) %>%
51 | list_rbind(names_to="file") %>%
52 | filter(fun %in% fun[duplicated(fun)],
53 | !str_detect(fun, "^unnamed_\\d+$")) %>%
54 | mutate(fun=paste0(fun, "()"), file=basename(as.character(file))) %>%
55 | arrange(fun)
56 |
57 | if(nrow(dups)>0){
58 | dup_list = dups %>%
59 | glue_data("{fun} in {file} (lines {first_line}-{last_line})") %>%
60 | set_names("*")
61 | if(verbose>0) cli_h2("Warning - Duplicates")
62 | cli_warn(c("x"="There is several functions with the same name."),
63 | class="autoimport_duplicate_warn")
64 | if(verbose>0) cli_inform(dup_list)
65 | }
66 | invisible(TRUE)
67 | }
68 |
--------------------------------------------------------------------------------
/R/ai_write.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | #' Take a dataframe from `autoimport_ask()`, a reflist from `autoimport_read()`, and
4 | #' a list of lines from `readr::read_lines()`, and compute for each file what importFrom
5 | #' lines should be removed or inserted.
6 | #' Writes the correct lines in `target_dir` so they can be reviewed in `import_review()`.
7 | #' Returns nothing of use.
8 | #' @noRd
9 | #' @keywords internal
10 | #' @importFrom cli cli_h1 cli_inform
11 | #' @importFrom fs file_delete
12 | autoimport_write = function(data_imports, ref_list, lines_list, location,
13 | ignore_package, pkg_name, target_dir, verbose){
14 |
15 | stopifnot(is.data.frame(data_imports))
16 | stopifnot(is.character(data_imports$pkg))
17 | stopifnot(names(ref_list)==names(lines_list))
18 | file_delete(dir(target_dir, full.names=TRUE))
19 |
20 | if(location=="function"){
21 | if(verbose>0) cli_h1("Writing at function level")
22 | if(verbose>1) cli_inform(c(">"="Temporarily writing to {.path {target_dir}}."))
23 | .autoimport_write_lvl_fn(data_imports, ref_list, lines_list,
24 | ignore_package, pkg_name, target_dir, verbose)
25 | } else {
26 | if(verbose>0) cli_h1("Writing at package level")
27 | if(verbose>1) cli_inform(c(">"="Temporarily writing to {.path {target_dir}}."))
28 | .autoimport_write_lvl_pkg(data_imports, ref_list, lines_list,
29 | ignore_package, pkg_name, target_dir, verbose)
30 | }
31 | }
32 |
33 |
34 | #' @noRd
35 | #' @keywords internal
36 | #' @importFrom dplyr filter mutate
37 | #' @importFrom fs path
38 | #' @importFrom glue glue
39 | #' @importFrom stringr str_ends
40 | .autoimport_write_lvl_pkg = function(data_imports, ref_list, lines_list,
41 | ignore_package, pkg_name, target_dir, verbose) {
42 | #merge all functions inserts into one (by setting source_fun)
43 | imports = data_imports %>%
44 | filter(!(ignore_package & str_ends(file, "-package.[Rr]"))) %>%
45 | mutate(source_fun="package_level") %>%
46 | get_inserts(exclude=c("base", "inner", pkg_name)) %>%
47 | unlist()
48 | inserts = glue("#' @importFrom {imports}")
49 |
50 | cur_package_doc = path("R", paste0(pkg_name, "-package"), ext="R")
51 | new_package_doc = path(target_dir, paste0(pkg_name, "-package"), ext="R")
52 |
53 | .copy_package_doc(cur_package_doc, new_package_doc)
54 | .add_autoimport_package_doc(new_package_doc)
55 | .update_package_doc(new_package_doc, inserts)
56 | .remove_fun_lvl_imports(lines_list, target_dir, except=cur_package_doc)
57 | TRUE
58 | }
59 |
60 |
61 | #' @noRd
62 | #' @keywords internal
63 | #' @importFrom cli cli_inform
64 | #' @importFrom dplyr setdiff
65 | #' @importFrom fs path
66 | #' @importFrom purrr imap map
67 | #' @importFrom stringr str_ends
68 | #' @importFrom tibble tibble
69 | .autoimport_write_lvl_fn = function(data_imports, ref_list, lines_list,
70 | ignore_package, pkg_name, target_dir, verbose) {
71 |
72 | # data_imports %>% filter(fun=="writeLines")
73 | # .x %>% filter(fun=="writeLines")
74 | #list of paths input/output
75 | #not used, could be a walk()
76 | paths = data_imports %>%
77 | split(list(.$file)) %>%
78 | map(~{
79 | cur_file = unique(.x$file)
80 | target_file = path(target_dir, basename(cur_file))
81 | stopifnot(length(cur_file)==1)
82 | lines = lines_list[[cur_file]]
83 | comments_refs = ref_list[[cur_file]]
84 |
85 | if(str_ends(cur_file, "-package.[Rr]") && ignore_package){
86 | if(verbose>0) cli_inform(c(v="Ignoring {.file {cur_file}}.
87 | Use {.code ignore_package=FALSE} to override."))
88 | return(NULL)
89 | }
90 | if(length(lines)==0){
91 | if(verbose>0) cli_inform(c(">"="Nothing done in {.file {cur_file}} (file is empty)"))
92 | return(NULL)
93 | }
94 |
95 | inserts = get_inserts(.x, exclude=c("base", "inner", pkg_name))
96 | if(verbose>0) cli_inform(c(i="{length(unlist(inserts))} insert{?s} in
97 | {.file {basename(cur_file)}}"))
98 |
99 | lines2 = comments_refs %>%
100 | imap(~get_lines2(.x, inserts[[.y]])) %>%
101 | unname() %>% unlist()
102 |
103 | if(identical(lines, lines2)){
104 | if(verbose>0) cli_inform(c(">"="Nothing done in {.file {cur_file}} (all is already OK)"))
105 | unlink(target_file)
106 | return(NULL)
107 | }
108 |
109 | n_new = setdiff(lines2, lines) %>% length()
110 | n_old = setdiff(lines, lines2) %>% length()
111 |
112 | write_utf8(target_file, lines2)
113 |
114 | if(verbose>0) cli_inform(c(v="Added {n_new} and removed {n_old} line{?s}
115 | from {.file {cur_file}}."))
116 | tibble(file=cur_file, target_file)
117 |
118 | })
119 |
120 | paths
121 | }
122 |
123 |
124 | # Utils pkg-level -----------------------------------------------------------------------------
125 |
126 |
127 | #' @noRd
128 | #' @keywords internal
129 | #' @importFrom fs file_exists
130 | #' @importFrom readr read_lines write_lines
131 | .copy_package_doc = function(cur_package_doc, new_package_doc){
132 | if(file_exists(cur_package_doc)){
133 | write_lines(read_lines(cur_package_doc), file=new_package_doc)
134 | }
135 | }
136 |
137 | #' @noRd
138 | #' @keywords internal
139 | #' @importFrom cli cli_inform
140 | #' @importFrom fs file_exists
141 | #' @importFrom readr read_lines write_lines
142 | #' @importFrom stringr str_detect
143 | .add_autoimport_package_doc = function(package_doc){
144 | if(!file_exists(package_doc)){
145 | cli_inform("Adding package-level documentation {.path {package_doc}}.")
146 | content = ""
147 | } else {
148 | content = read_lines(package_doc)
149 | }
150 | if(any(str_detect(content, "autoimport namespace: start"))){
151 | return(TRUE)
152 | }
153 |
154 | content = c(content, "",
155 | "# The following block is used by autoimport.",
156 | "## autoimport namespace: start",
157 | "## autoimport namespace: end",
158 | "NULL")
159 | write_lines(content, package_doc)
160 | }
161 |
162 | #' @noRd
163 | #' @keywords internal
164 | #' @importFrom readr read_lines write_lines
165 | #' @importFrom stringr str_detect
166 | .update_package_doc = function(package_doc, inserts){
167 | content = read_lines(package_doc)
168 | start = str_detect(content, "autoimport namespace: start") %>% which()
169 | stop = str_detect(content, "autoimport namespace: end") %>% which()
170 | if(length(start)==0) start = length(content)
171 | if(length(stop)==0) stop = length(content)
172 |
173 | new_content = c(content[1:start], inserts, content[stop:length(content)])
174 | write_lines(new_content, package_doc)
175 | }
176 |
177 | #' remove all `@importFrom` tags from source
178 | #' @importFrom fs path path_abs
179 | #' @importFrom purrr imap
180 | #' @importFrom readr write_lines
181 | #' @importFrom stringr str_starts
182 | #' @noRd
183 | #' @keywords internal
184 | .remove_fun_lvl_imports = function(lines_list, target_dir, except){
185 | lines_list %>%
186 | imap(function(lines, filename){
187 | if(path_abs(filename) %in% path_abs(except)) return(FALSE)
188 | target_file = path(target_dir, basename(filename))
189 | rmv = str_starts(lines, "#+' *@importFrom")
190 | new_lines = lines[!rmv]
191 | write_lines(new_lines, target_file)
192 | TRUE
193 | })
194 | }
195 |
196 | # Utils ---------------------------------------------------------------------------------------
197 |
198 |
199 | #' @importFrom dplyr arrange distinct filter mutate
200 | #' @importFrom purrr map
201 | #' @noRd
202 | #' @keywords internal
203 | get_inserts = function(.x, exclude){
204 | .x %>%
205 | filter(!is.na(pkg) & !pkg %in% exclude) %>%
206 | mutate(label = paste(pkg, paste(sort(unique(fun)), collapse=" ")),
207 | .by=c(pkg, source_fun)) %>%
208 | distinct(source_fun, label) %>%
209 | arrange(source_fun, label) %>%
210 | split(.$source_fun) %>%
211 | map(~.x$label)
212 | }
213 |
214 | #' @importFrom glue glue
215 | #' @importFrom stringr str_starts
216 | #' @noRd
217 | #' @keywords internal
218 | get_lines2 = function(src_ref, imports){
219 | fun_c = as.character(src_ref)
220 | if(length(imports)==0) return(fun_c)
221 | insert = glue("#' @importFrom {imports}")
222 |
223 | if(is_reexport(fun_c)){
224 | #TODO improve reexport management
225 | return(fun_c)
226 | }
227 |
228 | rmv = str_starts(fun_c, "#+' *@importFrom")
229 | if(any(rmv)){
230 | pos = min(which(rmv))
231 | fun_c = fun_c[!rmv]
232 | } else {
233 | x = parse(text=fun_c, keep.source=TRUE) %>% get_srcref_lines()
234 | stopifnot(length(x)==1)
235 | pos = x[[1]]$first_line_fun
236 | }
237 | insert_line(fun_c, insert, pos=pos)
238 | }
239 |
240 | #' @param lines result of [read_lines()]
241 | #' @param insert lines to insert
242 | #' @param pos insert before this position
243 | #' @noRd
244 | #' @keywords internal
245 | insert_line = function(lines, insert, pos){
246 | if(length(lines)==1 || pos==1){
247 | return(c(insert, lines))
248 | }
249 |
250 | c(
251 | lines[seq(1, pos-1)],
252 | insert,
253 | lines[seq(pos, length(lines))]
254 | )
255 | }
256 |
257 |
258 | #' @importFrom dplyr last
259 | #' @importFrom stringr str_detect
260 | #' @noRd
261 | #' @keywords internal
262 | is_reexport = function(fun_c){
263 | last_call = last(fun_c)
264 | str_detect(last_call, "(\\w+):{1,3}(?!:)(.+)") &&
265 | !str_detect(last_call, "(^|\\W)function\\(")
266 | }
267 |
--------------------------------------------------------------------------------
/R/assertions.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | #' @noRd
4 | #' @keywords internal
5 | #' @examples
6 | #' assert(1+1==2)
7 | #' assert(1+1==4)
8 | #' @importFrom cli cli_abort
9 | #' @importFrom glue glue
10 | #' @importFrom rlang caller_arg
11 | assert = function(x, msg=NULL){
12 | if(is.null(msg)){
13 | x_str = caller_arg(x)
14 | msg = glue("`{x_str}` is FALSE")
15 | }
16 | if(!x){
17 | cli_abort(msg)
18 | }
19 | invisible(TRUE)
20 | }
21 |
22 |
23 | #' @noRd
24 | #' @keywords internal
25 | #' @examples
26 | #' assert_file_exists(c("R/assertions.R", "R/autoimport.R"))
27 | #' assert_file_exists(c("R/assertions.SAS", "R/autoimport.SAS", "R/autoimport.R"))
28 | #' @importFrom cli cli_abort
29 | #' @importFrom fs file_exists
30 | assert_file_exists = function(x, msg=NULL){
31 | not_found = x[!file_exists(x)]
32 | if(length(not_found)>0){
33 | cli_abort("File{?s} do{?es/} not exist: {.file {not_found}}")
34 | }
35 | invisible(TRUE)
36 | }
37 |
--------------------------------------------------------------------------------
/R/autoimport-package.R:
--------------------------------------------------------------------------------
1 | utils::globalVariables(c(".", "x", "y", "fun", "pkg", "value", "values", "ind",
2 | "tmp", "label", "action", "token", "text", "what", "from",
3 | "fun_imported", "pkg_n_imports", "pkg_in_desc", "old_files", "changed",
4 | "pref_pkg", "package", "pkg_bak", "cache_dir", "defined_in_importlist",
5 | "details", "fun_already_imported", "fun_is_base", "fun_is_inner",
6 | "fun_is_private", "operator", "sessionInfo", "source_fun", "pkg_str"))
7 |
8 | # x="cache_dir defined_in_importlist details fun_already_imported"
9 | # str_split_1(x, "\\s+") %>% cat(sep='", "')
10 |
11 |
12 | #' @keywords internal
13 | #' @name autoimport-package
14 | #' @aliases autoimport-package
15 | ## usethis namespace: start
16 | #' @importFrom dplyr %>%
17 | #' @importFrom cli qty
18 | ## usethis namespace: end
19 | "_PACKAGE"
20 |
21 |
22 | main_caller = rlang::env()
23 |
--------------------------------------------------------------------------------
/R/autoimport.R:
--------------------------------------------------------------------------------
1 |
2 | #' Automatically compute `@importFrom` tags
3 | #'
4 | #' Automatically read all `R` files and compute appropriate `@importFrom` tags in the roxygen2 headers.
5 | #' The tags can be added to the source files using the [import_review()] shiny app afterward.
6 | #'
7 | #' @param root Path to the root of the package.
8 | #' @param location Whether to add `@importFrom` dispatched above each function, or centralized at the package level.
9 | #' @param files Files to read. Default to the `R/` folder.
10 | #' @param namespace_file Path to the NAMESPACE file
11 | #' @param description_file Path to the DESCRIPTION file
12 | #' @param use_cache Whether to use the cache system. Can only be "read" or "write".
13 | #' @param ignore_package Whether to ignore files ending with `-package.R`
14 | #' @param verbose The higher, the more output printed. May slow the process a bit.
15 | #' @param ... unused
16 | #'
17 | #' @return Mostly used for side effects. Invisibly returns a dataframe summarizing the function imports, with input arguments as attributes.
18 | #' @export
19 | #'
20 | #' @section Limitations:
21 | #' Autoimport is based on [utils::getSrcref()] and share the same limits.
22 | #' Therefore, some function syntaxes are not recognized and `autoimport` will try to remove their `@importFrom` from individual functions:
23 | #'
24 | #' - Operators (`@importFrom dplyr %>%`, `@importFrom rlang :=`, ...)
25 | #' - Functions called by name (e.g. `sapply(x, my_fun))`
26 | #' - Functions used inside strings (e.g. `glue("my_fun={my_fun(x)}")`)
27 | #'
28 | #' To keep them imported, you should either use a prefix (`pkg::my_fun`) or import them in your package-level documentation, as this file is ignored by default (with `ignore_package=TRUE`).
29 | #'
30 | #' @importFrom cli cli_abort cli_h1 cli_inform
31 | #' @importFrom dplyr setdiff
32 | #' @importFrom fs file_exists path path_dir
33 | #' @importFrom purrr map walk
34 | #' @importFrom rlang check_dots_empty check_installed current_env set_names
35 | #' @importFrom utils sessionInfo
36 | autoimport = function(root=".",
37 | ...,
38 | location=c("function", "package"),
39 | files=get_R_dir(root),
40 | namespace_file="NAMESPACE",
41 | description_file="DESCRIPTION",
42 | use_cache=TRUE, ignore_package=TRUE,
43 | verbose=2){
44 | target_dir = get_target_dir()
45 | check_dots_empty()
46 | ns = parse_namespace(namespace_file)
47 | location = match.arg(location)
48 | importlist_path = getOption("autoimport_importlist", path(root, "inst/IMPORTLIST"))
49 | cache_path = get_cache_path(root)
50 | if(file_exists(path(root, namespace_file))) namespace_file = path(root, namespace_file)
51 | if(file_exists(path(root, description_file))) description_file = path(root, description_file)
52 | if(!all(file_exists(files))) files = path(root, "R", files)
53 |
54 | description = desc::desc(file=description_file)
55 | deps = description$get_deps()
56 | pkg_name = unname(description$get("Package"))
57 |
58 | main_caller$env = current_env()
59 | if(isTRUE(use_cache)) use_cache = c("read", "write")
60 |
61 | ns_loading = deps$package %>% setdiff("R")
62 | check_installed(ns_loading)
63 | walk(ns_loading, register_namespace)
64 | if(verbose>0){
65 | cli_h1("Init")
66 | cli_inform(c("Autoimporting for package {.pkg {pkg_name}} at {.path {root}}"))
67 | cli_inform(c(v="Registered namespaces of {length(ns_loading)} dependencies."))
68 | }
69 | if(any(!file_exists(files))){
70 | cli_abort("Couldn't find file{?s} {.file {files[!file_exists(files)]}}")
71 | }
72 |
73 | files = set_names(files)
74 | lines_list = map(files, readr::read_lines)
75 |
76 | ref_list = autoimport_read(lines_list, verbose)
77 |
78 | data_imports = autoimport_parse(ref_list, cache_path, use_cache, pkg_name,
79 | ns, deps, verbose)
80 |
81 | data_imports = autoimport_ask(data_imports, ns, importlist_path, verbose)
82 |
83 | ai_write = autoimport_write(data_imports, ref_list, lines_list, location,
84 | ignore_package, pkg_name, target_dir, verbose)
85 | if(verbose>0) cli_h1("Finished")
86 |
87 | data_files = review_files(path_dir(files))
88 | review_dir = unique(path_dir(files))[1]
89 | if(verbose>0){
90 | if(!any(data_files$changed)){
91 | cli_inform(c(v="No changes to review."))
92 | } else {
93 | cli_inform(c(v="To view the diff and choose whether or not accepting the changes, run:",
94 | i='{.run autoimport::import_review("{review_dir}")}'))
95 | }
96 | }
97 |
98 | data_imports = structure(
99 | data_imports,
100 | root=normalizePath(root),
101 | files=unname(files),
102 | namespace_file=namespace_file,
103 | description_file=description_file,
104 | pkg_name=pkg_name,
105 | use_cache=use_cache, ignore_package=ignore_package,
106 | verbose=verbose,
107 |
108 | target_dir=target_dir,
109 | review_dir=review_dir,
110 | cache_path=cache_path,
111 | session_info=sessionInfo()
112 | )
113 |
114 | invisible(data_imports)
115 | }
116 |
--------------------------------------------------------------------------------
/R/decision.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | #' Decision management
4 | #'
5 | #' Opens a Shiny app that shows a visual diff of each modified file.
6 | #'
7 | #' @param source_path path to the original R files
8 | #' @param output_path path to the updated R files
9 | #' @param background whether to run the app in a background process. Default to `getOption("autoimport_background", FALSE)`.
10 | #'
11 | #' @section Warning:
12 | #' Beware that using `background=TRUE` can bloat your system with multiple R session! \cr
13 | #' You should probably kill the process when you are done:
14 | #' ```r
15 | #' p=import_review(background=TRUE)
16 | #' p$kill()
17 | #' ```
18 | #'
19 | #' @return nothing if `background==FALSE`, the ([callr::process]) object if `background==TRUE`
20 | #' @source inspired by [testthat::snapshot_review()]
21 | #' @export
22 | #' @importFrom cli cli_inform
23 | #' @importFrom dplyr arrange desc
24 | #' @importFrom rlang check_installed
25 | #' @importFrom stringr str_ends
26 | import_review = function(source_path="R/",
27 | output_path=get_target_dir(),
28 | background=getOption("autoimport_background", FALSE)) {
29 | check_installed("shiny", "for `import_review()` to work")
30 | check_installed("diffviewer", "for `import_review()` to work")
31 | data_files = review_files(source_path, output_path) %>%
32 | arrange(desc(str_ends(old_files, "package.[Rr]")))
33 |
34 | if(!any(data_files$changed)){
35 | cli_inform("No changes to review.")
36 | return(invisible(FALSE))
37 | }
38 |
39 | go = function(data_files){
40 | data_files %>%
41 | filter(changed) %>%
42 | review_app()
43 | rstudio_tickle()
44 | }
45 |
46 | if(isTRUE(background)){
47 | check_installed("callr", "for `import_review()` to work in background")
48 | brw = Sys.getenv("R_BROWSER")
49 | x=callr::r_bg(go, args=list(data_files=data_files),
50 | stdout="out", stderr="errors",
51 | package="autoimport", env = c(R_BROWSER=brw))
52 | return(x)
53 | }
54 |
55 | go(data_files)
56 | invisible()
57 | }
58 |
59 |
60 |
61 | #' @importFrom digest digest
62 | #' @importFrom fs file_exists path
63 | #' @importFrom purrr map2_lgl
64 | #' @importFrom tibble tibble
65 | #' @noRd
66 | #' @keywords internal
67 | review_files = function(source_path="R/", output_path=get_target_dir()){
68 | old_files = dir(source_path, full.names=TRUE)
69 | assert_file_exists(old_files)
70 | new_files = path(output_path, basename(old_files))
71 | old_files = old_files[file_exists(new_files)]
72 | new_files = new_files[file_exists(new_files)]
73 | changed = map2_lgl(old_files, new_files, ~{
74 | !identical(digest(.x, file=TRUE), digest(.y, file=TRUE))
75 | })
76 | tibble(old_files, new_files, changed)
77 | }
78 |
79 |
80 |
81 | # Shiny ---------------------------------------------------------------------------------------
82 |
83 |
84 |
85 | #' @importFrom cli cli_inform
86 | #' @importFrom fs file_move
87 | #' @importFrom rlang set_names
88 | #' @noRd
89 | review_app = function(data_files){
90 | case_index = seq_along(data_files$old_files) %>% set_names(data_files$old_files)
91 | handled = rep(FALSE, length(case_index))
92 |
93 | ui = shiny::fluidPage(
94 | style = "margin: 0.5em",
95 | shiny::fluidRow(style = "display: flex",
96 | shiny::div(style = "flex: 1 1",
97 | shiny::selectInput("cases", NULL, case_index, width = "100%")),
98 | shiny::div(class = "btn-group", style = "margin-left: 1em; flex: 0 0 auto",
99 | shiny::actionButton("stop", "Stop", class="btn-danger"),
100 | shiny::actionButton("skip", "Skip"),
101 | shiny::actionButton("accept", "Accept", class="btn-success"))
102 | ),
103 | shiny::fluidRow(
104 | diffviewer::visual_diff_output("diff")
105 | )
106 | )
107 |
108 | server = function(input, output, session) {
109 | old_path = data_files$old_files
110 | new_path = data_files$new_files
111 |
112 | i = shiny::reactive(as.numeric(input$cases))
113 | output$diff = diffviewer::visual_diff_render({
114 | file = old_path[i()]
115 | new_file = new_path[i()]
116 | assert_file_exists(file)
117 | assert_file_exists(new_file)
118 | diffviewer::visual_diff(file, new_file)
119 | })
120 |
121 | shiny::observeEvent(input$accept, {
122 | cli_inform(c(">"="Accepting modification of '{.file {old_path[[i()]]}}'"))
123 | file_move(new_path[[i()]], old_path[[i()]])
124 | update_cases()
125 | })
126 | shiny::observeEvent(input$skip, {
127 | cli_inform(c(">"="Skipping file '{.file {old_path[[i()]]}}'"))
128 | i = next_case()
129 | shiny::updateSelectInput(session, "cases", selected = i)
130 | })
131 | shiny::observeEvent(input$stop, {
132 | cli_inform(c("x"="Stopping"))
133 | shiny::stopApp()
134 | })
135 |
136 | update_cases = function(){
137 | handled[[i()]] <<- TRUE
138 | i = next_case()
139 | shiny::updateSelectInput(session, "cases",
140 | choices = case_index[!handled],
141 | selected = i)
142 | }
143 | next_case = function(){
144 | if(all(handled)){
145 | cli_inform(c(v="Review complete"))
146 | shiny::stopApp()
147 | return()
148 | }
149 | remaining = case_index[!handled]
150 | next_cases = which(remaining > i())
151 | x = if(length(next_cases)==0) 1 else next_cases[[1]]
152 | remaining[[x]]
153 | }
154 | }
155 |
156 | cli_inform(c(
157 | "Starting Shiny app for modification review",
158 | i = "Use {.key Ctrl + C} or {.key Echap} to quit"
159 | ))
160 | shiny::runApp(
161 | shiny::shinyApp(ui, server),
162 | quiet = TRUE,
163 | launch.browser = shiny::paneViewer()
164 | )
165 | invisible()
166 | }
167 |
168 | # Helpers -----------------------------------------------------------------
169 |
170 |
171 | # testthat:::rstudio_tickle
172 | #' @importFrom rlang is_installed
173 | #' @noRd
174 | rstudio_tickle = function(){
175 | if (!is_installed("rstudioapi")) {
176 | return()
177 | }
178 | if (!rstudioapi::hasFun("executeCommand")) {
179 | return()
180 | }
181 | rstudioapi::executeCommand("vcsRefresh")
182 | rstudioapi::executeCommand("refreshFiles")
183 | }
184 |
--------------------------------------------------------------------------------
/R/importlist.R:
--------------------------------------------------------------------------------
1 |
2 | #' Update the `IMPORTLIST` file
3 | #'
4 | #' Update the `IMPORTLIST` file, which forces the import of some packages without asking.
5 | #'
6 | #' @param imports a list of imports with `key=function` and `value=package`
7 | #' @param path path to the `IMPORTLIST` file
8 | #'
9 | #' @return nothing
10 | #' @export
11 | #'
12 | #' @importFrom cli cli_inform
13 | #' @importFrom dplyr pull
14 | #' @importFrom fs dir_create file_create file_exists path_dir
15 | #' @importFrom tibble deframe
16 | #' @importFrom utils modifyList
17 | update_importlist = function(imports, path=NULL){
18 | if(is.null(path)) path = getOption("autoimport_importlist", "inst/IMPORTLIST")
19 | # path = normalizePath(path, mustWork = FALSE)
20 | if(!file_exists(path)){
21 | dir_create(path_dir(path))
22 | file_create(path)
23 | }
24 | old_imports = get_importlist(path) %>% deframe() %>% as.list()
25 | new_imports = imports %>% pull(pref_pkg, name=fun) %>% as.list()
26 | if(length(new_imports)==0){
27 | cli_inform(c(i="No change needed to {.file {path}}"))
28 | return(FALSE)
29 | }
30 |
31 | file_content = modifyList(old_imports, new_imports)
32 | file_content = file_content[order(names(file_content))]
33 | output = paste0(names(file_content), " = ", file_content)
34 | writeLines(output, path)
35 | cli_inform(c(i="{length(new_imports)} line{?s} added to {.file {path}}"))
36 | TRUE
37 | }
38 |
39 |
40 | #' @rdname update_importlist
41 | #' @importFrom fs file_exists
42 | #' @importFrom purrr map map_chr
43 | #' @importFrom stringr str_split_1 str_squish str_starts
44 | #' @importFrom tibble tibble
45 | get_importlist = function(path=NULL){
46 | if(is.null(path)) path = getOption("autoimport_importlist", "inst/IMPORTLIST")
47 | if(!file_exists(path)) return(tibble(fun=NA, pref_pkg=NA))
48 |
49 | lines = readLines(path, warn=FALSE, encoding="UTF-8") %>%
50 | subset(.!="" & !str_starts(., "#")) %>%
51 | map(~str_split_1(.x, "=")) %>%
52 | map(~str_squish(.x))
53 | assert(all(lengths(lines)==2))
54 |
55 | #TODO check that file is correct and warn for xxx=unkwown_package
56 | tibble(fun=map_chr(lines, 1), pref_pkg=map_chr(lines, 2))
57 | }
58 |
--------------------------------------------------------------------------------
/R/utils.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | ref_names = c("first_line", "first_byte", "last_line", "last_byte", "first_column",
4 | "last_column", "first_parsed", "last_parsed")
5 |
6 | #TODO usethis:::read_utf8() ?
7 |
8 | #' @source usethis:::write_utf8
9 | #' @noRd
10 | write_utf8 = function (path, lines, append=FALSE, line_ending="\n") {
11 | stopifnot(is.character(path))
12 | stopifnot(is.character(lines))
13 | file_mode = if (append) "ab" else "wb"
14 | con = file(path, open=file_mode, encoding="utf-8")
15 | on.exit(close(con))
16 | lines = gsub("\r?\n", line_ending, lines)
17 | writeLines(enc2utf8(lines), con, sep = line_ending,
18 | useBytes = TRUE)
19 | invisible(TRUE)
20 | }
21 |
22 |
23 | #' roxygen2:::comments
24 | #' @noRd
25 | #' @importFrom purrr map
26 | comments = function (refs) {
27 | if(length(refs)==0) return(list())
28 | stopifnot(length(map(refs, ~attr(.x, "srcfile")) %>% unique())==1)
29 | srcfile = attr(refs[[1]], "srcfile")
30 |
31 | com = vector("list", length(refs))
32 | for (i in seq_along(refs)) {
33 | if (i == 1) {
34 | first_line = 1
35 | } else {
36 | first_line = refs[[i - 1]][3] + 1 #modif: +1
37 | }
38 | if (i == length(refs)){#add trailing lines
39 | last_line = length(srcfile$lines)
40 | last_byte = length(charToRaw(last(srcfile$lines)))
41 | } else {
42 | last_line = refs[[i]][3]
43 | last_byte = refs[[i]][4]
44 | }
45 | lloc = c(first_line, first_byte=1, last_line, last_byte)
46 | com[[i]] = srcref(srcfile, lloc)
47 | attr(com[[i]], "lines") = c(first_line, last_line)
48 | }
49 | com
50 | }
51 |
52 |
53 | #' @importFrom purrr map map2
54 | #' @importFrom utils getSrcref
55 | #' @noRd
56 | #' @examples
57 | #' lines = read_lines(file)
58 | #' parsed = parse(text=lines, keep.source=TRUE)
59 | get_srcref_lines = function(parsed){
60 | refs = getSrcref(parsed) %>% set_names_ref()
61 | comments_refs = comments(refs) %>% set_names_ref()
62 | ref_names = c("first_line", "first_byte", "last_line", "last_byte", "first_column",
63 | "last_column", "first_parsed", "last_parsed")
64 | # lst(
65 | # coms = comments_refs %>% map(~as.list(as.numeric(.x)) %>% set_names(ref_names)),
66 | # funs = refs %>% map(~as.list(as.numeric(.x)) %>% set_names(ref_names)),
67 | # ) %>% transpose()
68 |
69 | comments_refs %>% map(~list(first_line_com=.x[1], last_line=.x[3]))
70 | refs %>% map(~list(first_line_fun=.x[1], last_line=.x[3]))
71 |
72 | rtn = map2(comments_refs, refs, ~{
73 | last_line = max(.x[3], .y[3])
74 | list(first_line_com=.x[1], first_line_fun=.y[1], last_line=last_line)
75 | })
76 | attr(rtn, "src") = comments_refs
77 | rtn
78 | # lst(
79 | # coms = comments_refs %>% map(~list(first_line=.x[1], last_line=.x[3])),
80 | # funs = refs %>% map(~list(first_line=.x[1], last_line=.x[3])),
81 | # ) %>% transpose()
82 | }
83 |
84 |
85 |
86 | #' @importFrom stringr str_starts
87 | #' @noRd
88 | is_com = function(x) str_starts(x, "#+'")
89 |
90 | #' @importFrom purrr map_chr
91 | #' @importFrom rlang set_names
92 | #' @importFrom stringr regex str_extract str_starts
93 | #' @noRd
94 | set_names_ref = function(refs, warn_guess=FALSE){
95 | ref_names = refs %>%
96 | map_chr(~{
97 | src = as.character(.x, useSource=TRUE)
98 | src = src[!str_starts(src, "#")]
99 | src = src[nzchar(src)]
100 | # fun = paste(src, collapse="\n")
101 | # fun_name = str_extract(fun, regex("`?(.*?)`? *(?:=|<-) *function.*"), group=TRUE)
102 | fun_name = str_extract(src[1], regex("`?(.*?)`? *(?:=|<-) *function.*"), group=TRUE)
103 | # if(is.na(fun_name)){
104 | # if(warn_guess) {
105 | # cli_warn(c("Could not guess function name in code:", i="{.code {src}}"))
106 | # }
107 | # fun_name = "unknown"
108 | # }
109 | fun_name
110 | })
111 | ref_names[is.na(ref_names)] = paste0("unnamed_", seq_along(ref_names[is.na(ref_names)]))
112 |
113 | set_names(refs, ref_names)
114 | }
115 |
116 |
117 | #' A rewrite around [utils::getAnywhere()]
118 | #'
119 | #' Used in [parse_ref()], requires using `register_namespace()` beforehand.
120 | #' Find all the packages that hold a function. `utils::getAnywhere()` annoyingly uses `find()` which yields false positives.
121 | #'
122 | #' @param fun a function name (character)
123 | #' @param add_pkgs packages to look into, added to `loadedNamespaces()` (character)
124 | #'
125 | #' @return a character vector of package names
126 | #' @importFrom purrr keep map_lgl
127 | #' @importFrom rlang set_names
128 | #' @noRd
129 | get_anywhere = function(fun, add_pkgs=NULL){
130 | pkgs = c(loadedNamespaces(), add_pkgs) %>% unique() %>% set_names() %>%
131 | map_lgl(~is_exported(fun, pkg=.x)) %>% keep(isTRUE) %>% names() %>% sort()
132 | pkgs
133 | }
134 |
135 |
136 | #' @importFrom rlang ns_env
137 | #' @noRd
138 | register_namespace = function(name){
139 | suppressPackageStartupMessages(suppressWarnings(loadNamespace(name)))
140 | TRUE
141 | }
142 |
143 |
144 | #' is_exported("div", "htmltools")
145 | #' is_exported("div", "shiny")
146 | #' is_exported("dfsdsf", "shiny")
147 | #' @importFrom cli cli_abort
148 | #' @importFrom rlang is_installed
149 | #' @noRd
150 | is_exported = function(fun, pkg, type="::", fail=FALSE){
151 | if(!is_installed(pkg)){
152 | if(fail) cli_abort("{.pkg {pkg}} is not installed")
153 | return(FALSE)
154 | }
155 | text = paste0(pkg, type, fun)
156 | f = try(eval(parse(text=text)), silent=TRUE)
157 | is.function(f)
158 | }
159 |
160 |
161 | #' @noRd
162 | get_base_packages = function(){
163 | # rownames(installed.packages(priority="base")) %>% dput()
164 | c("base", "compiler", "datasets", "graphics", "grDevices", "grid",
165 | "methods", "parallel", "splines", "stats", "stats4", "tcltk",
166 | "tools", "utils")
167 | }
168 |
169 |
170 |
171 |
172 | # https://stackoverflow.com/a/31675695/3888000
173 | #' @noRd
174 | exists2 = function(x) {
175 | stopifnot(is.character(x) && length(x) == 1)
176 |
177 | split = strsplit(x, "::")[[1]]
178 |
179 | if (length(split) == 1) {
180 | exists(split[1])
181 | } else if (length(split) == 2) {
182 | exists(split[2], envir = asNamespace(split[1]))
183 | } else {
184 | stop(paste0("exists2 cannot handle ", x))
185 | }
186 | }
187 |
188 |
189 |
190 | #' @importFrom fs path_dir
191 | #' @importFrom stringr regex str_remove
192 | #' @noRd
193 | get_new_file = function(file, path=path_dir(file), prefix="", suffix=""){
194 | f = str_remove(basename(file), regex("\\.[rR]"))
195 | rtn=paste0(path, "/", prefix, f, suffix, ".R")
196 | if(rtn==file){
197 | stop("overwriting?")
198 | }
199 | rtn
200 | }
201 |
202 |
203 | #' @noRd
204 | #' @importFrom fs path
205 | get_R_dir = function(root="."){
206 | path = path(root, "R")
207 | dir(path, pattern="\\.[Rr]$", full.names=TRUE)
208 | }
209 | #' @noRd
210 | #' @importFrom fs dir_create path path_temp
211 | get_target_dir = function(path=NULL){
212 | tmp = path_temp("autoimport_temp_target_dir")
213 | d = getOption("autoimport_target_dir", tmp)
214 | if(!is.null(path)) d = path(d, path)
215 | dir_create(d)
216 | d
217 | }
218 | #' @noRd
219 | #' @importFrom fs path
220 | get_cache_path = function(root="."){
221 | getOption("autoimport_cache_path", path(root, "inst/autoimport_cache.rds"))
222 | }
223 |
224 | #' @noRd
225 | #' @keywords internal
226 | #' @importFrom cli cli_abort
227 | clean_cache = function(root="."){
228 | cache_file = get_cache_path(root)
229 | rslt = unlink(cache_dir, recursive=TRUE)
230 | if(rslt==1){
231 | cli_abort("Could not remove {.file {cache_file}}.")
232 | }
233 | invisible(TRUE)
234 | }
235 |
236 |
237 | #' because base::parseNamespaceFile() is not very handy for my use.
238 | #' @importFrom cli cli_abort
239 | #' @importFrom dplyr arrange filter mutate rename select
240 | #' @importFrom purrr map map_chr
241 | #' @importFrom tibble tibble
242 | #' @importFrom tidyr complete
243 | #' @noRd
244 | #' @keywords internal
245 | parse_namespace = function(file){
246 | directives = parse(file, keep.source = FALSE, srcfile = NULL) %>% as.list()
247 | rtn = tibble(operator = map_chr(directives, ~as.character(.x[1])),
248 | value = map_chr(directives, ~as.character(.x[2])),
249 | details = map_chr(directives, ~as.character(.x[3]))) %>%
250 | mutate(operator=factor(operator, levels=c("export", "import", "importFrom"))) %>%
251 | complete(operator) %>%
252 | split(.$operator) %>%
253 | map(~.x %>% filter(!is.na(value)))
254 |
255 | rtn$export = rtn$export %>% select(-details)
256 | rtn$import = rtn$import %>% rename(except=details)
257 | rtn$importFrom = rtn$importFrom %>% rename(from=value, what=details)
258 |
259 | if(anyDuplicated(rtn$importFrom$what)!=0){
260 | x = rtn$importFrom %>%
261 | filter(what %in% what[duplicated(what)]) %>%
262 | arrange(what, from)
263 | label = paste(x$from, x$what, sep="::")
264 | cli_abort(c("Duplicate `importFrom` mention in {.file {file}}",
265 | i="{.fun {label}}"),
266 | class="autoimport_namespace_dup_error",
267 | call=main_caller$env)
268 | }
269 | rtn
270 | }
271 |
272 |
273 | #' @source vctrs::`%0%`
274 | #' @noRd
275 | #' @keywords internal
276 | `%0%` = function (x, y) {
277 | if(length(x)==0L) y else x
278 | }
279 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # autoimport
2 |
3 |
4 |
5 | [](http://www.gnu.org/licenses/gpl-3.0.html)
6 | [](https://lifecycle.r-lib.org/articles/stages.html#stable)
7 | [](https://CRAN.R-project.org/package=autoimport)
8 | [](https://github.com/DanChaltiel/autoimport)
9 | [](https://github.com/DanChaltiel/autoimport/actions/workflows/R-CMD-check.yaml)
10 |
11 |
12 |
13 | `autoimport` is a package designed to easily add `@importFrom` roxygen tags to all your functions.
14 |
15 |
16 | ## Concept
17 |
18 | When importing functions into a package, the [R Packages (2e)](https://r-pkgs.org/dependencies-in-practice.html#in-code-below-r) guidelines recommend using `@importFrom`, either above each function or in a dedicated section of the package-level documentation.
19 |
20 | But let's be honest for a second, this is one of the most tedious tasks ever, isn't it?
21 | And we are devs, we love automating things, don't we?
22 |
23 | Meet `autoimport`!
24 | It parses your code, detects all imported functions, and adds the appropriate `@importFrom` tags in the right place. Just like that!
25 |
26 |
27 | ## Installation
28 |
29 | Install either from the stable version from CRAN or the dev version from GitHub:
30 |
31 | ``` r
32 | # Install from CRAN
33 | pak::pak("autoimport")
34 | # Install from Github
35 | pak::pak("DanChaltiel/autoimport")
36 | ```
37 |
38 |
39 | ## Getting started
40 |
41 | Just run the function, it's showtime!
42 |
43 | ``` r
44 | devtools::load_all(".")
45 | autoimport::autoimport() #location="function" by default
46 | #autoimport::autoimport(location="package")
47 | ```
48 |
49 | The first run might take some time, but a cache system is implemented so that next runs are faster.
50 |
51 | Afterward, you can see the diff and accept the changes using the shiny widget:
52 |
53 | ``` r
54 | autoimport::import_review()
55 | ```
56 |
57 | However, a picture is worth a thousand words:
58 |
59 | 
60 |
61 | As you could probably tell, the shiny widget is ~~stolen from~~ inspired by `testthat::snapshot_review()`. Many thanks for them for this gem!
62 |
63 | ## Important notes
64 |
65 | - `autoimport` will guess the potential source of your functions based on (1) the packages currently loaded in your environment (e.g. via `library()`), and (2) the packages listed as dependencies in DESCRIPTION.
66 |
67 | - `load_all(".")` is required for autoimport to have access to the package's private functions, for example so that `dplyr::filter()` cannot mask `yourpackage:::filter()`.
68 |
69 | - Some package guesses are bound to be wrong, in which case you should use `usethis::use_import_from()`. See "Limitations" below for more details.
70 |
71 |
72 | ## Limitations
73 |
74 | Autoimport is based on `utils::getSrcref()` and share the same limits. Therefore, some function syntaxes are not recognized and `autoimport` will try to remove their `@importFrom` from individual functions:
75 |
76 | - Operators (`@importFrom dplyr %>%`, `@importFrom rlang :=`, ...)
77 | - Functions called by name (e.g. `sapply(x, my_fun))`
78 | - Functions used inside strings (e.g. `glue("my_fun={my_fun(x)}")`)
79 |
80 | To keep them imported, you should either use a prefix (`pkg::my_fun`) or import them in your package-level documentation, as this file is ignored by default (due to `ignore_package=TRUE`).
81 |
82 | For that, `usethis::use_import_from()` and `usethis::use_pipe()` are your friends!
83 |
84 | ## Cache system
85 |
86 | As running `autoimport()` on a large package can take some time, a cache system is implemented, by default in file `inst/autoimport_cache.rds`.
87 |
88 | Any function not modified since last run should be taken from the cache, resulting on a much faster run.
89 |
90 | In some seldom cases, this can cause issues with modifications in DESCRIPTION or IMPORTLIST not being taken into account. Run `clean_cache()` to remove this file, or use `use_cache="write"`.
91 |
92 |
93 | ## Algorithm
94 |
95 | When trying to figure out which package to import a function from, `autoimport()` follows this algorithm:
96 |
97 | - If the function is prefixed with the package, ignore
98 | - Else, if the function is already mentioned in NAMESPACE, use the package
99 | - Else, if the function is exported by only one package, use this package
100 | - Else, ask the user from which package to import the function
101 | - Else, warn that the function was not found
102 |
103 | Note that this algorithm is still a bit experimental and that I could only test it on my few own packages. Any feedback is more than welcome!
104 |
105 |
106 | ## Style
107 |
108 | As I couldn't find any standardized guideline about the right order of `roxygen2` tags (#30), `autoimport` puts them:
109 |
110 | - in place of the first `@importFrom` tag if there is one
111 | - just before the function call otherwise
112 |
113 |
--------------------------------------------------------------------------------
/_pkgdown.yml:
--------------------------------------------------------------------------------
1 | url: https://danchaltiel.github.io/autoimport/
2 | template:
3 | bootstrap: 5
4 |
5 |
--------------------------------------------------------------------------------
/autoimport.Rproj:
--------------------------------------------------------------------------------
1 | Version: 1.0
2 | ProjectId: dea34b22-6613-4cd2-a53f-7df414510a76
3 |
4 | RestoreWorkspace: No
5 | SaveWorkspace: No
6 | AlwaysSaveHistory: Yes
7 |
8 | EnableCodeIndexing: Yes
9 | UseSpacesForTab: Yes
10 | NumSpacesForTab: 2
11 | Encoding: UTF-8
12 |
13 | RnwWeave: knitr
14 | LaTeX: XeLaTeX
15 |
16 | AutoAppendNewline: Yes
17 | StripTrailingWhitespace: Yes
18 | LineEndingConversion: Posix
19 |
20 | BuildType: Package
21 | PackageUseDevtools: Yes
22 | PackageInstallArgs: --no-multiarch --with-keep.source
23 | PackageRoxygenize: rd,collate,namespace
24 |
--------------------------------------------------------------------------------
/codemeta.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
3 | "@type": "SoftwareSourceCode",
4 | "identifier": "autoimport",
5 | "description": "A toolbox to read all R files inside a package and automatically generate @importFrom 'roxygen2' tags in the right place. Includes a 'shiny' application to review the changes before applying them.",
6 | "name": "autoimport: Automatic Generation of @importFrom Tags",
7 | "relatedLink": "https://danchaltiel.github.io/autoimport/",
8 | "codeRepository": "https://github.com/DanChaltiel/autoimport",
9 | "issueTracker": "https://github.com/DanChaltiel/autoimport/issues",
10 | "license": "https://spdx.org/licenses/GPL-3.0",
11 | "version": "0.1.1",
12 | "programmingLanguage": {
13 | "@type": "ComputerLanguage",
14 | "name": "R",
15 | "url": "https://r-project.org"
16 | },
17 | "runtimePlatform": "R version 4.4.1 (2024-06-14 ucrt)",
18 | "author": [
19 | {
20 | "@type": "Person",
21 | "givenName": "Dan",
22 | "familyName": "Chaltiel",
23 | "email": "dan.chaltiel@gmail.com",
24 | "@id": "https://orcid.org/0000-0003-3488-779X"
25 | }
26 | ],
27 | "maintainer": [
28 | {
29 | "@type": "Person",
30 | "givenName": "Dan",
31 | "familyName": "Chaltiel",
32 | "email": "dan.chaltiel@gmail.com",
33 | "@id": "https://orcid.org/0000-0003-3488-779X"
34 | }
35 | ],
36 | "softwareSuggestions": [
37 | {
38 | "@type": "SoftwareApplication",
39 | "identifier": "callr",
40 | "name": "callr",
41 | "provider": {
42 | "@id": "https://cran.r-project.org",
43 | "@type": "Organization",
44 | "name": "Comprehensive R Archive Network (CRAN)",
45 | "url": "https://cran.r-project.org"
46 | },
47 | "sameAs": "https://CRAN.R-project.org/package=callr"
48 | },
49 | {
50 | "@type": "SoftwareApplication",
51 | "identifier": "covr",
52 | "name": "covr",
53 | "provider": {
54 | "@id": "https://cran.r-project.org",
55 | "@type": "Organization",
56 | "name": "Comprehensive R Archive Network (CRAN)",
57 | "url": "https://cran.r-project.org"
58 | },
59 | "sameAs": "https://CRAN.R-project.org/package=covr"
60 | },
61 | {
62 | "@type": "SoftwareApplication",
63 | "identifier": "devtools",
64 | "name": "devtools",
65 | "provider": {
66 | "@id": "https://cran.r-project.org",
67 | "@type": "Organization",
68 | "name": "Comprehensive R Archive Network (CRAN)",
69 | "url": "https://cran.r-project.org"
70 | },
71 | "sameAs": "https://CRAN.R-project.org/package=devtools"
72 | },
73 | {
74 | "@type": "SoftwareApplication",
75 | "identifier": "knitr",
76 | "name": "knitr",
77 | "provider": {
78 | "@id": "https://cran.r-project.org",
79 | "@type": "Organization",
80 | "name": "Comprehensive R Archive Network (CRAN)",
81 | "url": "https://cran.r-project.org"
82 | },
83 | "sameAs": "https://CRAN.R-project.org/package=knitr"
84 | },
85 | {
86 | "@type": "SoftwareApplication",
87 | "identifier": "pkgload",
88 | "name": "pkgload",
89 | "provider": {
90 | "@id": "https://cran.r-project.org",
91 | "@type": "Organization",
92 | "name": "Comprehensive R Archive Network (CRAN)",
93 | "url": "https://cran.r-project.org"
94 | },
95 | "sameAs": "https://CRAN.R-project.org/package=pkgload"
96 | },
97 | {
98 | "@type": "SoftwareApplication",
99 | "identifier": "rstudioapi",
100 | "name": "rstudioapi",
101 | "provider": {
102 | "@id": "https://cran.r-project.org",
103 | "@type": "Organization",
104 | "name": "Comprehensive R Archive Network (CRAN)",
105 | "url": "https://cran.r-project.org"
106 | },
107 | "sameAs": "https://CRAN.R-project.org/package=rstudioapi"
108 | },
109 | {
110 | "@type": "SoftwareApplication",
111 | "identifier": "testthat",
112 | "name": "testthat",
113 | "version": ">= 3.0.0",
114 | "provider": {
115 | "@id": "https://cran.r-project.org",
116 | "@type": "Organization",
117 | "name": "Comprehensive R Archive Network (CRAN)",
118 | "url": "https://cran.r-project.org"
119 | },
120 | "sameAs": "https://CRAN.R-project.org/package=testthat"
121 | },
122 | {
123 | "@type": "SoftwareApplication",
124 | "identifier": "tidyverse",
125 | "name": "tidyverse",
126 | "provider": {
127 | "@id": "https://cran.r-project.org",
128 | "@type": "Organization",
129 | "name": "Comprehensive R Archive Network (CRAN)",
130 | "url": "https://cran.r-project.org"
131 | },
132 | "sameAs": "https://CRAN.R-project.org/package=tidyverse"
133 | }
134 | ],
135 | "softwareRequirements": {
136 | "1": {
137 | "@type": "SoftwareApplication",
138 | "identifier": "R",
139 | "name": "R",
140 | "version": ">= 3.6.0"
141 | },
142 | "2": {
143 | "@type": "SoftwareApplication",
144 | "identifier": "cli",
145 | "name": "cli",
146 | "provider": {
147 | "@id": "https://cran.r-project.org",
148 | "@type": "Organization",
149 | "name": "Comprehensive R Archive Network (CRAN)",
150 | "url": "https://cran.r-project.org"
151 | },
152 | "sameAs": "https://CRAN.R-project.org/package=cli"
153 | },
154 | "3": {
155 | "@type": "SoftwareApplication",
156 | "identifier": "desc",
157 | "name": "desc",
158 | "provider": {
159 | "@id": "https://cran.r-project.org",
160 | "@type": "Organization",
161 | "name": "Comprehensive R Archive Network (CRAN)",
162 | "url": "https://cran.r-project.org"
163 | },
164 | "sameAs": "https://CRAN.R-project.org/package=desc"
165 | },
166 | "4": {
167 | "@type": "SoftwareApplication",
168 | "identifier": "diffviewer",
169 | "name": "diffviewer",
170 | "provider": {
171 | "@id": "https://cran.r-project.org",
172 | "@type": "Organization",
173 | "name": "Comprehensive R Archive Network (CRAN)",
174 | "url": "https://cran.r-project.org"
175 | },
176 | "sameAs": "https://CRAN.R-project.org/package=diffviewer"
177 | },
178 | "5": {
179 | "@type": "SoftwareApplication",
180 | "identifier": "digest",
181 | "name": "digest",
182 | "provider": {
183 | "@id": "https://cran.r-project.org",
184 | "@type": "Organization",
185 | "name": "Comprehensive R Archive Network (CRAN)",
186 | "url": "https://cran.r-project.org"
187 | },
188 | "sameAs": "https://CRAN.R-project.org/package=digest"
189 | },
190 | "6": {
191 | "@type": "SoftwareApplication",
192 | "identifier": "dplyr",
193 | "name": "dplyr",
194 | "provider": {
195 | "@id": "https://cran.r-project.org",
196 | "@type": "Organization",
197 | "name": "Comprehensive R Archive Network (CRAN)",
198 | "url": "https://cran.r-project.org"
199 | },
200 | "sameAs": "https://CRAN.R-project.org/package=dplyr"
201 | },
202 | "7": {
203 | "@type": "SoftwareApplication",
204 | "identifier": "fs",
205 | "name": "fs",
206 | "provider": {
207 | "@id": "https://cran.r-project.org",
208 | "@type": "Organization",
209 | "name": "Comprehensive R Archive Network (CRAN)",
210 | "url": "https://cran.r-project.org"
211 | },
212 | "sameAs": "https://CRAN.R-project.org/package=fs"
213 | },
214 | "8": {
215 | "@type": "SoftwareApplication",
216 | "identifier": "glue",
217 | "name": "glue",
218 | "provider": {
219 | "@id": "https://cran.r-project.org",
220 | "@type": "Organization",
221 | "name": "Comprehensive R Archive Network (CRAN)",
222 | "url": "https://cran.r-project.org"
223 | },
224 | "sameAs": "https://CRAN.R-project.org/package=glue"
225 | },
226 | "9": {
227 | "@type": "SoftwareApplication",
228 | "identifier": "purrr",
229 | "name": "purrr",
230 | "provider": {
231 | "@id": "https://cran.r-project.org",
232 | "@type": "Organization",
233 | "name": "Comprehensive R Archive Network (CRAN)",
234 | "url": "https://cran.r-project.org"
235 | },
236 | "sameAs": "https://CRAN.R-project.org/package=purrr"
237 | },
238 | "10": {
239 | "@type": "SoftwareApplication",
240 | "identifier": "readr",
241 | "name": "readr",
242 | "provider": {
243 | "@id": "https://cran.r-project.org",
244 | "@type": "Organization",
245 | "name": "Comprehensive R Archive Network (CRAN)",
246 | "url": "https://cran.r-project.org"
247 | },
248 | "sameAs": "https://CRAN.R-project.org/package=readr"
249 | },
250 | "11": {
251 | "@type": "SoftwareApplication",
252 | "identifier": "rlang",
253 | "name": "rlang",
254 | "provider": {
255 | "@id": "https://cran.r-project.org",
256 | "@type": "Organization",
257 | "name": "Comprehensive R Archive Network (CRAN)",
258 | "url": "https://cran.r-project.org"
259 | },
260 | "sameAs": "https://CRAN.R-project.org/package=rlang"
261 | },
262 | "12": {
263 | "@type": "SoftwareApplication",
264 | "identifier": "shiny",
265 | "name": "shiny",
266 | "provider": {
267 | "@id": "https://cran.r-project.org",
268 | "@type": "Organization",
269 | "name": "Comprehensive R Archive Network (CRAN)",
270 | "url": "https://cran.r-project.org"
271 | },
272 | "sameAs": "https://CRAN.R-project.org/package=shiny"
273 | },
274 | "13": {
275 | "@type": "SoftwareApplication",
276 | "identifier": "stringr",
277 | "name": "stringr",
278 | "provider": {
279 | "@id": "https://cran.r-project.org",
280 | "@type": "Organization",
281 | "name": "Comprehensive R Archive Network (CRAN)",
282 | "url": "https://cran.r-project.org"
283 | },
284 | "sameAs": "https://CRAN.R-project.org/package=stringr"
285 | },
286 | "14": {
287 | "@type": "SoftwareApplication",
288 | "identifier": "tibble",
289 | "name": "tibble",
290 | "provider": {
291 | "@id": "https://cran.r-project.org",
292 | "@type": "Organization",
293 | "name": "Comprehensive R Archive Network (CRAN)",
294 | "url": "https://cran.r-project.org"
295 | },
296 | "sameAs": "https://CRAN.R-project.org/package=tibble"
297 | },
298 | "15": {
299 | "@type": "SoftwareApplication",
300 | "identifier": "tidyr",
301 | "name": "tidyr",
302 | "provider": {
303 | "@id": "https://cran.r-project.org",
304 | "@type": "Organization",
305 | "name": "Comprehensive R Archive Network (CRAN)",
306 | "url": "https://cran.r-project.org"
307 | },
308 | "sameAs": "https://CRAN.R-project.org/package=tidyr"
309 | },
310 | "16": {
311 | "@type": "SoftwareApplication",
312 | "identifier": "utils",
313 | "name": "utils"
314 | },
315 | "SystemRequirements": null
316 | },
317 | "fileSize": "511.987KB",
318 | "releaseNotes": "https://github.com/DanChaltiel/autoimport/blob/master/NEWS.md",
319 | "readme": "https://github.com/DanChaltiel/autoimport/blob/main/README.md",
320 | "contIntegration": "https://github.com/DanChaltiel/autoimport/actions/workflows/R-CMD-check.yaml",
321 | "developmentStatus": "https://lifecycle.r-lib.org/articles/stages.html#stable"
322 | }
323 |
--------------------------------------------------------------------------------
/cran-comments.md:
--------------------------------------------------------------------------------
1 | ## R CMD check results
2 |
3 | 0 errors | 0 warnings | 1 note
4 |
5 | * This is a new release.
6 | * Given the scope of this package, there is no relevant example to be written. You just call the function and let the CLI guide you to the next step.
7 |
--------------------------------------------------------------------------------
/inst/IMPORTLIST:
--------------------------------------------------------------------------------
1 | as_tibble = dplyr
2 | attr = base
3 | check_dots_empty = rlang
4 | desc = dplyr
5 | dir_create = fs
6 | div = shiny
7 | file_exists = fs
8 | filter = dplyr
9 | isFALSE = base
10 | keep = purrr
11 | lag = dplyr
12 | map = purrr
13 | ns_env = rlang
14 | order = base
15 | path = fs
16 | print = base
17 | read_lines = readr
18 | readLines = base
19 | regex = stringr
20 | set_names = rlang
21 | setdiff = base
22 | starts_with = dplyr
23 | unname = base
24 | which = base
25 | write_lines = readr
26 | writeLines = base
27 |
--------------------------------------------------------------------------------
/inst/WORDLIST:
--------------------------------------------------------------------------------
1 | CMD
2 | IMPORTLIST
3 | Lifecycle
4 | ORCID
5 | Roxygen
6 | WIP
7 | importFrom
8 | roxygen
9 | syntaxes
10 |
--------------------------------------------------------------------------------
/inst/figures/autoimport_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DanChaltiel/autoimport/851e6822cc286c0dbab6049bc1e8f7774a060a86/inst/figures/autoimport_bg.png
--------------------------------------------------------------------------------
/inst/figures/autoimport_gimp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DanChaltiel/autoimport/851e6822cc286c0dbab6049bc1e8f7774a060a86/inst/figures/autoimport_gimp.png
--------------------------------------------------------------------------------
/inst/figures/autoimport_gimp.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DanChaltiel/autoimport/851e6822cc286c0dbab6049bc1e8f7774a060a86/inst/figures/autoimport_gimp.xcf
--------------------------------------------------------------------------------
/inst/figures/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DanChaltiel/autoimport/851e6822cc286c0dbab6049bc1e8f7774a060a86/inst/figures/logo.png
--------------------------------------------------------------------------------
/inst/figures/showcase.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DanChaltiel/autoimport/851e6822cc286c0dbab6049bc1e8f7774a060a86/inst/figures/showcase.gif
--------------------------------------------------------------------------------
/inst/hex.R:
--------------------------------------------------------------------------------
1 |
2 | library(hexSticker)
3 | library(ggplot2)
4 |
5 |
6 | d=tibble(
7 | # x = c(0, 1, -1, 2, -2, 3, -3, 4, -4),
8 | # y = 1:9,
9 | x = c(0, 1, -1, 2, -2, 3, -3),
10 | y = 1:7,
11 | hjust = case_when(sign(x)==1 ~ 1, sign(x)==-1 ~ 0, .default=0.5),
12 | # hjust = (sign(x)+1)*0.5-1
13 | )
14 |
15 | d = tibble(x = c(0, 1, -1, 2, -2, 3, -3),
16 | y = 1:7)
17 |
18 | p = d %>%
19 | mutate(x=x*0.1) %>%
20 | ggplot() +
21 | aes(x, y, hjust=0.5) +
22 | geom_text(label="@importFrom", color = "#c26132", family="mono", size=9) +
23 | scale_x_continuous(expand=c(0.2, 0.2), breaks=scales::breaks_width(1)) +
24 | scale_y_continuous(expand=c(0.2, 0.2), breaks=scales::breaks_width(1)) +
25 | theme_void()
26 |
27 | sticker(
28 | #package name
29 | package="autoimport",
30 | p_size=20,
31 | #hexagon
32 | h_fill = "#323232",
33 | h_color = "#c26132",
34 | h_size = 1,
35 | #subplot
36 | subplot= p,
37 | s_x=1, s_y=.75,
38 | s_width=1.5, s_height=1.1,
39 | #output
40 | filename="inst/figures/logo.png"
41 | )
42 |
43 |
44 |
45 |
46 | # GIMP background -----------------------------------------------------------------------------
47 |
48 | #
49 | # # img = "inst/figures/importfrom.png"
50 | # img = ggplot()+theme_void()
51 | # sticker(
52 | # img,
53 | # package="",
54 | # p_size=16,
55 | # s_x=1, s_y=.7,
56 | # h_fill = "#323232",
57 | # h_color = "#c26132",
58 | # h_size = 1,
59 | # s_width=0.8, s_height=1,
60 | #
61 | # filename="inst/figures/autoimport_bg.png"
62 | # ) %>% print()
63 |
64 |
65 |
--------------------------------------------------------------------------------
/man/autoimport-package.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/autoimport-package.R
3 | \docType{package}
4 | \name{autoimport-package}
5 | \alias{autoimport-package}
6 | \title{autoimport: Automatic Generation of @importFrom Tags}
7 | \description{
8 | A toolbox to read all R files inside a package and automatically generate @importFrom 'roxygen2' tags in the right place. Includes a shiny application to review the changes before applying them.
9 | }
10 | \seealso{
11 | Useful links:
12 | \itemize{
13 | \item \url{https://github.com/DanChaltiel/autoimport}
14 | \item \url{https://danchaltiel.github.io/autoimport/}
15 | \item Report bugs at \url{https://github.com/DanChaltiel/autoimport/issues}
16 | }
17 |
18 | }
19 | \author{
20 | \strong{Maintainer}: Dan Chaltiel \email{dan.chaltiel@gmail.com} (\href{https://orcid.org/0000-0003-3488-779X}{ORCID})
21 |
22 | }
23 | \keyword{internal}
24 |
--------------------------------------------------------------------------------
/man/autoimport.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/autoimport.R
3 | \name{autoimport}
4 | \alias{autoimport}
5 | \title{Automatically compute \verb{@importFrom} tags}
6 | \usage{
7 | autoimport(
8 | root = ".",
9 | ...,
10 | location = c("function", "package"),
11 | files = get_R_dir(root),
12 | namespace_file = "NAMESPACE",
13 | description_file = "DESCRIPTION",
14 | use_cache = TRUE,
15 | ignore_package = TRUE,
16 | verbose = 2
17 | )
18 | }
19 | \arguments{
20 | \item{root}{Path to the root of the package.}
21 |
22 | \item{...}{unused}
23 |
24 | \item{location}{Whether to add \verb{@importFrom} dispatched above each function, or centralized at the package level.}
25 |
26 | \item{files}{Files to read. Default to the \verb{R/} folder.}
27 |
28 | \item{namespace_file}{Path to the NAMESPACE file}
29 |
30 | \item{description_file}{Path to the DESCRIPTION file}
31 |
32 | \item{use_cache}{Whether to use the cache system. Can only be "read" or "write".}
33 |
34 | \item{ignore_package}{Whether to ignore files ending with \code{-package.R}}
35 |
36 | \item{verbose}{The higher, the more output printed. May slow the process a bit.}
37 | }
38 | \value{
39 | Mostly used for side effects. Invisibly returns a dataframe summarizing the function imports, with input arguments as attributes.
40 | }
41 | \description{
42 | Automatically read all \code{R} files and compute appropriate \verb{@importFrom} tags in the roxygen2 headers.
43 | The tags can be added to the source files using the \code{\link[=import_review]{import_review()}} shiny app afterward.
44 | }
45 | \section{Limitations}{
46 |
47 | Autoimport is based on \code{\link[utils:sourceutils]{utils::getSrcref()}} and share the same limits.
48 | Therefore, some function syntaxes are not recognized and \code{autoimport} will try to remove their \verb{@importFrom} from individual functions:
49 | \itemize{
50 | \item Operators (\verb{@importFrom dplyr \%>\%}, \verb{@importFrom rlang :=}, ...)
51 | \item Functions called by name (e.g. \verb{sapply(x, my_fun))}
52 | \item Functions used inside strings (e.g. \code{glue("my_fun={my_fun(x)}")})
53 | }
54 |
55 | To keep them imported, you should either use a prefix (\code{pkg::my_fun}) or import them in your package-level documentation, as this file is ignored by default (with \code{ignore_package=TRUE}).
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/man/import_review.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/decision.R
3 | \name{import_review}
4 | \alias{import_review}
5 | \title{Decision management}
6 | \source{
7 | inspired by \code{\link[testthat:snapshot_accept]{testthat::snapshot_review()}}
8 | }
9 | \usage{
10 | import_review(
11 | source_path = "R/",
12 | output_path = get_target_dir(),
13 | background = getOption("autoimport_background", FALSE)
14 | )
15 | }
16 | \arguments{
17 | \item{source_path}{path to the original R files}
18 |
19 | \item{output_path}{path to the updated R files}
20 |
21 | \item{background}{whether to run the app in a background process. Default to \code{getOption("autoimport_background", FALSE)}.}
22 | }
23 | \value{
24 | nothing if \code{background==FALSE}, the (\link[callr:reexports]{callr::process}) object if \code{background==TRUE}
25 | }
26 | \description{
27 | Opens a Shiny app that shows a visual diff of each modified file.
28 | }
29 | \section{Warning}{
30 |
31 | Beware that using \code{background=TRUE} can bloat your system with multiple R session! \cr
32 | You should probably kill the process when you are done:
33 |
34 | \if{html}{\out{}}\preformatted{p=import_review(background=TRUE)
35 | p$kill()
36 | }\if{html}{\out{
}}
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/man/update_importlist.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/importlist.R
3 | \name{update_importlist}
4 | \alias{update_importlist}
5 | \alias{get_importlist}
6 | \title{Update the \code{IMPORTLIST} file}
7 | \usage{
8 | update_importlist(imports, path = NULL)
9 |
10 | get_importlist(path = NULL)
11 | }
12 | \arguments{
13 | \item{imports}{a list of imports with \verb{key=function} and \code{value=package}}
14 |
15 | \item{path}{path to the \code{IMPORTLIST} file}
16 | }
17 | \value{
18 | nothing
19 | }
20 | \description{
21 | Update the \code{IMPORTLIST} file, which forces the import of some packages without asking.
22 | }
23 |
--------------------------------------------------------------------------------
/tests/testthat.R:
--------------------------------------------------------------------------------
1 | # This file is part of the standard setup for testthat.
2 | # It is recommended that you do not modify it.
3 | #
4 | # Where should you do additional test configuration?
5 | # Learn more about the roles of various files in:
6 | # * https://r-pkgs.org/tests.html
7 | # * https://testthat.r-lib.org/reference/test_package.html#special-files
8 |
9 | library(testthat)
10 | library(autoimport)
11 |
12 | test_check("autoimport")
13 |
--------------------------------------------------------------------------------
/tests/testthat/helper-init.R:
--------------------------------------------------------------------------------
1 |
2 | # Options -------------------------------------------------------------------------------------
3 |
4 | Sys.setenv(LANGUAGE = "en")
5 | Sys.setenv(TZ='Europe/Paris')
6 |
7 | options(
8 | encoding="UTF-8",
9 | warn=1, #0=stacks (default), 1=immediate=TRUE, 2 =error
10 | rlang_backtrace_on_error = "full",
11 | stringsAsFactors=FALSE,
12 | dplyr.summarise.inform=FALSE,
13 | tidyverse.quiet=TRUE,
14 | tidyselect_verbosity ="verbose",#quiet or verbose
15 | lifecycle_verbosity="warning", #NULL, "quiet", "warning" or "error"
16 | testthat.progress.max_fails = 50,
17 | rlang_backtrace_on_error = "full"
18 | )
19 |
20 | snapshot_review_bg = function(...){
21 | brw = Sys.getenv("R_BROWSER")
22 | callr::r_bg(function() testthat::snapshot_review(...),
23 | package=TRUE,
24 | env = c(R_BROWSER = brw))
25 | }
26 |
27 | v=utils::View
28 | #'@source https://stackoverflow.com/a/52066708/3888000
29 | shhh = function(expr) suppressPackageStartupMessages(suppressWarnings(expr))
30 | shhh(library(tidyverse))
31 | shhh(library(rlang))
32 |
33 |
34 | # Directories ---------------------------------------------------------------------------------
35 |
36 | test_path = function(path){
37 | if(!str_detect(getwd(), "testthat")){
38 | path = paste0("tests/testthat/", path)
39 | }
40 | path
41 | }
42 |
43 | options(
44 | autoimport_warnings_files_basename=TRUE,
45 | autoimport_testing_ask_save_importlist=NULL,
46 | autoimport_testing_dont_ask_select_first=NULL,
47 | autoimport_importlist=NULL,
48 | autoimport_target_dir=NULL
49 | )
50 |
51 |
52 | # Helpers -------------------------------------------------------------------------------------
53 |
54 | #helper for snapshots
55 | poor_diff = function(file){
56 | file_old = test_path("source", file)
57 | file_new = test_path("output", file)
58 | assert_file_exists(file_old)
59 | if(!file_exists(file_new)) return(NULL)
60 |
61 | a = readLines(file_old)
62 | b = readLines(file_new)
63 | common = intersect(a, b)
64 | adds = setdiff(b, a)
65 | removals = setdiff(a, b)
66 |
67 | lst(common, adds, removals)
68 | }
69 |
70 | expect_imported = function(output, pkg, fun){
71 | needle = glue("^#' ?@importFrom.*{pkg}.*{fun}")
72 | a = str_extract(output, glue("^#' ?@importFrom(.*){fun}"), group=1) %>%
73 | na.omit() %>% stringr::str_trim()
74 | b = if(length(a)>0) (", but from {{{a}}}.") else "."
75 | msg = cli::format_inline("Function {.fn {fun}} not imported from {{{pkg}}}", b)
76 | expect(any(str_detect(output, needle)),
77 | failure_message=msg)
78 | invisible(output)
79 | }
80 |
81 | expect_not_imported = function(output, pkg, fun){
82 | needle = glue("^#' ?@importFrom.*{pkg}.*{fun}")
83 | x = str_detect(output, needle)
84 | faulty = line = NULL
85 | if(any(x)){
86 | line = min(which(str_detect(output, needle)))
87 | faulty = output[line]
88 | }
89 | msg = cli::format_inline("Function `{fun}` imported from `{pkg}` on line {line}: {.val {faulty}}.")
90 | expect(!any(x), failure_message=msg)
91 |
92 | invisible(faulty)
93 | }
94 |
95 | test_autoimport = function(files, bad_ns=FALSE, use_cache=FALSE, root=NULL, ..., verbose=2){
96 | #reset file paths
97 | if(is.null(root)){
98 | dir_source = test_path("source") %>% normalizePath()
99 | nm = paste0("autoimport_test_", format(Sys.time(), "%Y-%m-%d_%H-%M-%S"))
100 | root = path(tempdir(), nm)
101 | unlink(root, recursive=TRUE)
102 | dir_create(root)
103 | file.copy(dir(dir_source, full.names=TRUE), to=root, recursive=TRUE)
104 | # dir(root, full.names=TRUE, recursive=TRUE)
105 | }
106 | wd = setwd(root)
107 | on.exit(setwd(wd))
108 |
109 | #load the whole test namespace
110 | pkgload::load_all(path=root, helpers=FALSE, quiet=TRUE)
111 |
112 | #set options
113 | rlang::local_options(
114 | rlang_backtrace_on_error = "full",
115 | autoimport_testing_dont_ask_select_first = TRUE,
116 | autoimport_testing_ask_save_importlist = 2 #2=No, 1=Yes
117 | )
118 |
119 | #run
120 | ns = if(bad_ns) "BAD_NAMESPACE" else "NAMESPACE"
121 | autoimport(
122 | root=root,
123 | files=files,
124 | ignore_package=TRUE,
125 | use_cache=use_cache,
126 | namespace_file=ns,
127 | verbose=verbose,
128 | ...
129 | )
130 |
131 | }
132 |
133 | #diapo 3 donc en non-binding on est surpuissant ou c'est juste une paramétrisation ?
134 |
135 |
136 | #' @examples
137 | #' warn("hello", class="foobar") %>% expect_classed_conditions(warning_class="foo")
138 | expect_classed_conditions = function(expr, message_class=NULL, warning_class=NULL, error_class=NULL){
139 | dummy = c("rlang_message", "message", "rlang_warning", "warning", "rlang_error", "error", "condition")
140 | ms = list()
141 | ws = list()
142 | es = list()
143 | x = withCallingHandlers(
144 | withRestarts(expr, muffleStop=function() "expect_classed_conditions__error"),
145 | message=function(m){
146 | ms <<- c(ms, list(m))
147 | invokeRestart("muffleMessage")
148 | },
149 | warning=function(w){
150 | ws <<- c(ws, list(w))
151 | invokeRestart("muffleWarning")
152 | },
153 | error=function(e){
154 | es <<- c(es, list(e))
155 | invokeRestart("muffleStop")
156 | }
157 | )
158 |
159 | f = function(cond_list, cond_class){
160 | cl = map(cond_list, class) %>% purrr::flatten_chr()
161 | missing = setdiff(cond_class, cl) %>% setdiff(dummy)
162 | extra = setdiff(cl, cond_class) %>% setdiff(dummy)
163 | if(length(missing)>0 || length(extra)>0){
164 | cli_abort(c("{.arg {caller_arg(cond_class)}} is not matching thrown conditions:",
165 | i="Missing expected classes: {.val {missing}}",
166 | i="Extra unexpected classes: {.val {extra}}"),
167 | call=rlang::caller_env())
168 | }
169 | }
170 | f(es, error_class)
171 | f(ws, warning_class)
172 | f(ms, message_class)
173 | expect_true(TRUE)
174 | x
175 | }
176 |
177 | condition_overview = function(expr){
178 | tryCatch2(expr) %>% attr("overview")
179 | }
180 | tryCatch2 = function(expr){
181 | errors = list()
182 | warnings = list()
183 | messages = list()
184 | rtn = withCallingHandlers(tryCatch(expr, error = function(e) {
185 | errors <<- c(errors, list(e))
186 | return("error")
187 | }), warning = function(w) {
188 | warnings <<- c(warnings, list(w))
189 | invokeRestart("muffleWarning")
190 | }, message = function(m) {
191 | messages <<- c(messages, list(m))
192 | invokeRestart("muffleMessage")
193 | })
194 | attr(rtn, "errors") = unique(map_chr(errors, conditionMessage))
195 | attr(rtn, "warnings") = unique(map_chr(warnings, conditionMessage))
196 | attr(rtn, "messages") = unique(map_chr(messages, conditionMessage))
197 | x = c(errors, warnings, messages) %>% unique()
198 | attr(rtn, "overview") = tibble(type = map_chr(x, ~ifelse(inherits(.x,
199 | "error"), "Error", ifelse(inherits(.x, "warning"), "Warning",
200 | "Message"))), class = map_chr(x, ~class(.x) %>% glue::glue_collapse("/")),
201 | message = map_chr(x, ~conditionMessage(.x)))
202 | rtn
203 | }
204 |
205 |
206 | # All clear! ----------------------------------------------------------------------------------
207 |
208 | cli::cli_inform(c(v="Initializer {.file tests/testthat/helper-init.R} loaded",
209 | "is_testing={is_testing()}, is_parallel={is_parallel()}, interactive={interactive()}"))
210 |
--------------------------------------------------------------------------------
/tests/testthat/source/BAD_NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | importFrom(cli,cli_warn)
4 | importFrom(devtools,as.package)
5 | importFrom(dplyr,n)
6 | importFrom(rlang,abort)
7 | importFrom(shiny,a)
8 | importFrom(purrr,map)
9 | importFrom(dplyr,lag)
10 | importFrom(stats,lag)
11 |
--------------------------------------------------------------------------------
/tests/testthat/source/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: autoimport_test
2 | Version: 0.0.1.9000
3 | Title: Automatically generate @importFrom roxygen tags
4 | Authors@R:
5 | c(person(given = "Dan",
6 | family = "Chaltiel",
7 | role = c("aut", "cre"),
8 | email = "dan.chaltiel@gmail.com",
9 | comment = c(ORCID = "0000-0003-3488-779X")))
10 | Description: A toolbox to read all R files inside a package and
11 | automatically generate @importFrom roxygen in the right place.
12 | License: GPL-3
13 | Depends:
14 | R (>= 3.6.0)
15 | Imports:
16 | cli,
17 | desc,
18 | diffviewer,
19 | digest,
20 | dplyr,
21 | glue,
22 | purrr,
23 | readr,
24 | rlang,
25 | shiny,
26 | stringr,
27 | tibble,
28 | tidyr,
29 | utils
30 | Suggests:
31 | callr,
32 | covr,
33 | devtools,
34 | knitr,
35 | pkgload,
36 | rstudioapi,
37 | testthat (>= 3.0.0),
38 | tidyverse
39 | Encoding: UTF-8
40 | Roxygen: list(markdown = TRUE)
41 | RoxygenNote: 7.3.2
42 | Config/testthat/edition: 3
43 |
--------------------------------------------------------------------------------
/tests/testthat/source/EMPTY_NAMESPACE:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DanChaltiel/autoimport/851e6822cc286c0dbab6049bc1e8f7774a060a86/tests/testthat/source/EMPTY_NAMESPACE
--------------------------------------------------------------------------------
/tests/testthat/source/NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | importFrom(cli,cli_warn)
4 | importFrom(devtools,as.package)
5 | importFrom(dplyr,n)
6 | importFrom(rlang,abort)
7 | importFrom(ggplot2,ggplot)
8 | importFrom(shiny,a)
9 | importFrom(purrr,map)
10 |
--------------------------------------------------------------------------------
/tests/testthat/source/R/sample_code-package.R:
--------------------------------------------------------------------------------
1 | #' @keywords internal
2 | "_PACKAGE"
3 |
4 | ## usethis namespace: start
5 | #' @importFrom dplyr %>%
6 | ## usethis namespace: end
7 | NULL
8 |
--------------------------------------------------------------------------------
/tests/testthat/source/R/sample_error.R:
--------------------------------------------------------------------------------
1 |
2 | #abort already imported in NAMESPACE
3 | abort = function(){
4 | 1
5 | }
6 |
7 | f = function(){
8 | abort()
9 | }
10 |
11 |
12 | f = function(){
13 | abort()
14 | }
15 |
--------------------------------------------------------------------------------
/tests/testthat/source/R/sample_funs.R:
--------------------------------------------------------------------------------
1 | 1
2 |
3 |
4 |
5 | #' Title f1
6 | #'
7 | #' a description
8 | #'
9 | #' @param x c
10 | #'
11 | #' @return ee
12 | #'
13 | #' @section a section:
14 | #' content
15 | #' @importFrom dplyr mutate
16 | #' @importFrom dplyr mutate_all
17 | #' @export
18 | #' @importFrom forcats as_factor
19 | #'
20 |
21 | #this is a useless comment line
22 | f1 = function(x){
23 | #private functions, should not be imported
24 | x = mutate(x, a=0) #remove existing import
25 | x = assert(x, TRUE)
26 | x = filter(x, TRUE)
27 | #explicit calls, should not be imported
28 | x = dplyr::arrange(x, TRUE)
29 | x = glue::glue(x, TRUE)
30 | #base function, should not be imported
31 | x = sum(x)
32 | x = date(x) #not from lubridate (IMPORTLIST)
33 | #other functions, should be imported
34 | x = pivot_longer(x, a=0)
35 | x = set_names(map(x), TRUE)
36 | x = div(x, TRUE) #from shiny, not html
37 | #juste a variable, should be ignored
38 | x = "#' @importFrom dplyr mutate"
39 | #inner function, should be ignored
40 | f = function(a) a
41 | x = f()
42 | g = if(TRUE) na.omit else identity
43 | x = g()
44 | #R6 function, should be ignored
45 | x = x$met()
46 | stop("ok")
47 | }
48 |
49 |
50 | #' Title f2
51 | #'
52 | #' This is f2
53 | #'
54 | #' @return ee
55 | #' @export
56 | #' @examples
57 | #' @importFrom rlang :=
58 | #' x=1
59 | f2 <- function(x){
60 | x := select(x, TRUE)
61 | stop("ok")
62 | }
63 |
64 |
65 | f3 <- function(x){
66 | x = select(x, TRUE)
67 | stop("ok")
68 | }
69 |
70 | #' @importFrom dplyr %>%
71 | #' @export
72 | dplyr::`%>%`
73 |
74 | 1
75 |
76 | #this is
77 | #a trailing comment
78 | #with multiple empty lines at EOF
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/tests/testthat/source/R/sample_funs2.R:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #duplicate function
6 | f2 = function(){
7 | 1
8 | }
9 | f2 = function(){
10 | ggplot()
11 | }
12 |
13 |
14 | #from roxygen, not in DESCRIPTION
15 | warn_in_desc = function(){
16 | x = rd_roclet()
17 | }
18 |
19 |
20 | #private function, should override dplyr::mutate
21 | mutate = function(){
22 | filter("foo")
23 | }
24 |
25 | #private function, should override dplyr::filter
26 | filter = function(){
27 | 1
28 | }
29 |
30 | #private function, should not conflict with autoimport:::assert
31 | assert = function(){
32 | 1
33 | }
34 |
35 |
36 | #function with inner function
37 | #' @importFrom dplyr filter
38 | foobar = function(){
39 | filter <- base::identity #inner function, should override autoimport::filter
40 | glimpse = function() 1
41 |
42 | filter("foo")
43 | glimpse("foo")
44 | abcdefgh()
45 | wxyz()
46 | bind_rows()
47 | }
48 |
49 |
50 | #this is
51 | #a trailing comment
52 | #with only one empty line at EOF
53 |
--------------------------------------------------------------------------------
/tests/testthat/source/inst/IMPORTLIST:
--------------------------------------------------------------------------------
1 | attr = base
2 | date = base
3 | xxxx = unknown_package
4 | filter = dplyr
5 | set_names = purrr
6 |
--------------------------------------------------------------------------------
/tests/testthat/test-ai_errors.R:
--------------------------------------------------------------------------------
1 |
2 | test_that("autoimport warnings", {
3 | ai = test_autoimport(files="sample_funs2.R") %>%
4 | suppressMessages() %>%
5 | expect_classed_conditions(warning_class=c("autoimport_duplicate_warn",
6 | "autoimport_fun_not_in_desc_warn",
7 | "autoimport_fun_not_found_warn"))
8 |
9 | target_dir = attr(ai, "target_dir")
10 | target_file = path(target_dir, "sample_funs2.R")
11 | expect_true(file_exists(target_dir))
12 |
13 | #test output
14 | out1 = readLines(target_file)
15 | expect_in(c("#this is", "#a trailing comment"), out1)
16 | })
17 |
18 |
19 | test_that("autoimport errors", {
20 | test_autoimport(files="sample_error.R") %>%
21 | suppressMessages() %>%
22 | expect_warning(class="autoimport_duplicate_warn") %>%
23 | expect_error(class="autoimport_conflict_import_private_error")
24 |
25 | test_autoimport(files=test_path("source/sample_error.R"),
26 | bad_ns=TRUE) %>%
27 | suppressMessages() %>%
28 | expect_error(class="autoimport_namespace_dup_error")
29 | })
30 |
--------------------------------------------------------------------------------
/tests/testthat/test-autoimport.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | test_that("autoimport works", {
4 | # ai = test_autoimport(files="sample_funs.R")
5 | ai = test_autoimport(files="sample_funs.R",
6 | verbose=0) %>%
7 | suppressMessages()
8 |
9 |
10 | #*WARNING* loading a library before running tests manually can cause
11 | #namespace problems with additional imports. For instance, run `library(broom)`
12 | # session_info = attr(ai, "session_info")
13 | # expect_false("broom" %in% names(session_info$otherPkgs))
14 |
15 |
16 | #test attributes: attributes(ai) %>% names()
17 | review_dir = attr(ai, "review_dir")
18 | expect_true(dir.exists(review_dir))
19 | target_dir = attr(ai, "target_dir")
20 | target_file = path(target_dir, "sample_funs.R")
21 | expect_true(file_exists(target_dir))
22 |
23 |
24 | #test output
25 | out1 = readLines(target_file)
26 |
27 | #private functions, should not be imported
28 | expect_not_imported(out1, "dplyr", "mutate")
29 | expect_not_imported(out1, "dplyr", "filter")
30 | expect_not_imported(out1, ".*", "assert")
31 |
32 | #explicit calls, should not be imported
33 | expect_not_imported(out1, "dplyr", "arrange")
34 | expect_not_imported(out1, "knitr", "asis_output")
35 |
36 | #base function, should not be imported
37 | expect_not_imported(out1, ".*", "sum")
38 | expect_not_imported(out1, ".* ", "date") #not lubridate (IMPORTLIST)
39 |
40 | #inner/private functions, should not be imported
41 | expect_not_imported(out1, "inner", ".*")
42 | expect_not_imported(out1, "autoimport_test", ".*")
43 |
44 | #other functions, should be imported
45 | expect_imported(out1, "purrr", "map")
46 | expect_imported(out1, "purrr", "set_names") #not rlang (IMPORTLIST)
47 | expect_imported(out1, "shiny", "div")
48 | expect_imported(out1, "tidyr", "pivot_longer")
49 | expect_not_imported(out1, "htmltools", "div")
50 | expect_not_imported(out1, "lubridate", "date")
51 |
52 | #leave trailing comment
53 | expect_in(c("#this is", "#a trailing comment"), out1)
54 | })
55 |
56 |
57 |
--------------------------------------------------------------------------------
/tests/testthat/test-cache.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | test_that("autoimport cache works", {
4 |
5 | #STEP 1: create cache
6 |
7 | files = c("sample_funs.R", "sample_funs2.R")
8 | ai_1 = test_autoimport(files, use_cache="write",
9 | verbose=0) %>%
10 | suppressWarnings() %>%
11 | expect_silent() #check that verbose=0 is silent
12 |
13 | expect_setequal(ai_1$ai_source, "file")
14 | root = attr(ai_1, "root")
15 | cache_path_1 = attr(ai_1, "cache_path")
16 | expect_true(file_exists(cache_path_1))
17 |
18 |
19 | #STEP 2: read cache from file
20 |
21 | ai_2 = test_autoimport(files, use_cache=TRUE, root=root,
22 | verbose=0) %>%
23 | suppressMessages() %>%
24 | suppressWarnings()
25 |
26 | expect_equal(attr(ai_2, "root"), root)
27 |
28 | expect_setequal(ai_2$ai_source, "cache_file")
29 |
30 | expect_equal(normalizePath(attr(ai_1, "cache_path")),
31 | normalizePath(attr(ai_2, "cache_path")))
32 |
33 |
34 | #STEP 3: modify a single function in a file
35 |
36 | mod_file_path = attr(ai_2, "files") %>% str_subset("funs2")
37 | mod_file = readLines(mod_file_path)
38 | fun_line = mod_file %>% str_detect("function()") %>% which() %>% min()
39 | add_line = " x = abs(x)"
40 | mod_file2 = c(mod_file[1:fun_line+1], add_line, mod_file[(fun_line+2):length(mod_file)])
41 | writeLines(mod_file2, mod_file_path)
42 |
43 |
44 | #STEP 4: read cache from file & refs, except for 1 function
45 |
46 | ai_3 = test_autoimport(files, use_cache="read", root=root,
47 | verbose=0) %>%
48 | suppressMessages() %>%
49 | suppressWarnings()
50 |
51 | cnt = ai_3 %>% count(ai_source) %>% pull(n, name=ai_source) %>% as.list()
52 | expect_true(cnt$file == 1) #only one function parsed again from file
53 | expect_true(cnt$cache_file > 1) #~15 functions read from the cached file
54 | expect_true(cnt$cache_ref > 1) #~7 functions read from cached refs
55 |
56 | })
57 |
--------------------------------------------------------------------------------
/tests/testthat/test-location.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | test_that("autoimport works at package level", {
4 | ai = test_autoimport(files="sample_funs.R",
5 | location="package",
6 | verbose=0) %>%
7 | suppressMessages()
8 |
9 |
10 | #*WARNING* loading a library before running tests manually can cause
11 | #namespace problems with additional imports. For instance, run `library(broom)`
12 | # session_info = attr(ai, "session_info")
13 | # expect_false("broom" %in% names(session_info$otherPkgs))
14 |
15 |
16 | #test attributes: attributes(ai) %>% names()
17 | review_dir = attr(ai, "review_dir")
18 | expect_true(dir.exists(review_dir))
19 | target_dir = attr(ai, "target_dir")
20 | expect_true(file_exists(target_dir))
21 | target_file = path(target_dir, "sample_funs.R")
22 | target_pkg_lvl_doc = path(target_dir, "autoimport_test-package.R")
23 |
24 |
25 | #test output
26 | out1 = readLines(target_file)
27 | out_pld = readLines(target_pkg_lvl_doc)
28 |
29 | #no imports at function-level documentation
30 | expect_not_imported(out1, ".*", ".*")
31 |
32 | expect_imported(out_pld, "purrr", "map")
33 | expect_imported(out_pld, "purrr", "set_names") #not rlang (IMPORTLIST)
34 | expect_imported(out_pld, "shiny", "div")
35 | expect_imported(out_pld, "tidyr", "pivot_longer")
36 |
37 | # import_review(review_dir, target_dir)
38 | })
39 |
40 |
41 |
--------------------------------------------------------------------------------