├── .Rbuildignore ├── .gitattributes ├── .github ├── .gitignore ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── R-CMD-check.yaml │ ├── bump_dev_version.yaml │ └── pkgdown.yaml ├── .gitignore ├── CRAN-SUBMISSION ├── DESCRIPTION ├── NAMESPACE ├── NEWS.md ├── R ├── ai_ask.R ├── ai_parse.R ├── ai_read.R ├── ai_write.R ├── assertions.R ├── autoimport-package.R ├── autoimport.R ├── decision.R ├── importlist.R └── utils.R ├── README.md ├── _pkgdown.yml ├── autoimport.Rproj ├── codemeta.json ├── cran-comments.md ├── inst ├── IMPORTLIST ├── WORDLIST ├── figures │ ├── autoimport_bg.png │ ├── autoimport_gimp.png │ ├── autoimport_gimp.xcf │ ├── logo.png │ └── showcase.gif └── hex.R ├── man ├── autoimport-package.Rd ├── autoimport.Rd ├── import_review.Rd └── update_importlist.Rd └── tests ├── testthat.R └── testthat ├── helper-init.R ├── source ├── BAD_NAMESPACE ├── DESCRIPTION ├── EMPTY_NAMESPACE ├── NAMESPACE ├── R │ ├── sample_code-package.R │ ├── sample_error.R │ ├── sample_funs.R │ └── sample_funs2.R └── inst │ └── IMPORTLIST ├── test-ai_errors.R ├── test-autoimport.R ├── test-cache.R └── test-location.R /.Rbuildignore: -------------------------------------------------------------------------------- 1 | .git 2 | .git/* 3 | man/figures/* 4 | examples/* 5 | 6 | 7 | TODO\.R 8 | DEV\.R 9 | ^R/dev-.*\.R$ 10 | ^R/knit_print_debug\.R$ 11 | 12 | .*~.* 13 | 14 | [.]rds$ 15 | 16 | ^README.Rmd$ 17 | ^\.lintr$ 18 | ^.*\.Rproj$ 19 | ^.*\.RData$ 20 | ^\.github$ 21 | ^\.Rproj\.user$ 22 | ^\.travis\.yml$ 23 | ^_pkgdown\.yml$ 24 | ^codecov\.yml$ 25 | ^cran-comments\.md$ 26 | ^doc$ 27 | ^Meta$ 28 | ^data-raw$ 29 | ^docs$ 30 | ^pkgdown$ 31 | ^CRAN-RELEASE$ 32 | ^codemeta\.json$ 33 | ^CRAN-SUBMISSION$ 34 | ^internal\.md$ 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | Please be as clear and thorough as possible. 12 | 13 | **Reproducible example** 14 | Share the code that caused the bug, along with all error and warning messages. 15 | Please also use `dput(df)` to provide a sample dataset so I can reproduce the error. If your dataset is too large, select a minimal number of columns and rows, or use https://pastebin.com/. 16 | 17 | **Session info** 18 |
19 | ``` r 20 | #Paste here the output of `sessionInfo()` here 21 | ``` 22 |
23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an improvement 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the new feature** 11 | Please be as clear and thorough as possible. 12 | 13 | **Known workaround** 14 | If you know any workaround, please share the code here. 15 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | 8 | name: R-CMD-check.yaml 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | R-CMD-check: 14 | runs-on: ${{ matrix.config.os }} 15 | 16 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | config: 22 | - {os: macos-latest, r: 'release'} 23 | - {os: windows-latest, r: 'release'} 24 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 25 | - {os: ubuntu-latest, r: 'release'} 26 | - {os: ubuntu-latest, r: 'oldrel-1'} 27 | 28 | env: 29 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 30 | R_KEEP_PKG_SOURCE: yes 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - uses: r-lib/actions/setup-pandoc@v2 36 | 37 | - uses: r-lib/actions/setup-r@v2 38 | with: 39 | r-version: ${{ matrix.config.r }} 40 | http-user-agent: ${{ matrix.config.http-user-agent }} 41 | use-public-rspm: true 42 | 43 | - uses: r-lib/actions/setup-r-dependencies@v2 44 | with: 45 | extra-packages: any::rcmdcheck 46 | needs: check 47 | 48 | - uses: r-lib/actions/check-r-package@v2 49 | with: 50 | upload-snapshots: true 51 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 52 | -------------------------------------------------------------------------------- /.github/workflows/bump_dev_version.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: 5 | - main 6 | name: Bump dev version 7 | 8 | jobs: 9 | update_version: 10 | runs-on: ubuntu-latest 11 | env: 12 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 13 | steps: 14 | - name: Check out repository 15 | uses: actions/checkout@v4 16 | - name: Set up R 17 | uses: r-lib/actions/setup-r@v2 18 | - name: Bump dev version 19 | uses: DanChaltiel/actions/bump-dev-version@v3 20 | with: 21 | create-tag: 'true' 22 | update-readme: 'true' 23 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | release: 8 | types: [published] 9 | workflow_dispatch: 10 | 11 | name: pkgdown.yaml 12 | 13 | permissions: read-all 14 | 15 | jobs: 16 | pkgdown: 17 | runs-on: ubuntu-latest 18 | # Only restrict concurrency for non-PR jobs 19 | concurrency: 20 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 21 | env: 22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 23 | permissions: 24 | contents: write 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - uses: r-lib/actions/setup-pandoc@v2 29 | 30 | - uses: r-lib/actions/setup-r@v2 31 | with: 32 | use-public-rspm: true 33 | 34 | - uses: r-lib/actions/setup-r-dependencies@v2 35 | with: 36 | extra-packages: any::pkgdown, local::. 37 | needs: website 38 | 39 | - name: Build site 40 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 41 | shell: Rscript {0} 42 | 43 | - name: Deploy to GitHub pages 🚀 44 | if: github.event_name != 'pull_request' 45 | uses: JamesIves/github-pages-deploy-action@v4.5.0 46 | with: 47 | clean: false 48 | branch: gh-pages 49 | folder: docs 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # rds files 4 | *.rds 5 | 6 | #Dev files 7 | DEV\.R 8 | TODO\.R 9 | todo* 10 | .Rproj.user 11 | 12 | *bak* 13 | *local* 14 | errors 15 | out 16 | tests/testthat/output* 17 | internal.md 18 | docs 19 | -------------------------------------------------------------------------------- /CRAN-SUBMISSION: -------------------------------------------------------------------------------- 1 | Version: 0.1.1 2 | Date: 2025-02-01 09:32:04 UTC 3 | SHA: 3ad7266b3b2f6dd76e0896efb5e24aab19557ee2 4 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: autoimport 2 | Version: 0.1.1.9002 3 | Title: Automatic Generation of @importFrom 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 'roxygen2' tags in the right place. 12 | Includes a 'shiny' application to review the changes before applying them. 13 | License: GPL-3 14 | URL: https://github.com/DanChaltiel/autoimport, https://danchaltiel.github.io/autoimport/ 15 | BugReports: https://github.com/DanChaltiel/autoimport/issues 16 | Depends: 17 | R (>= 3.6.0) 18 | Imports: 19 | cli, 20 | desc, 21 | diffviewer, 22 | digest, 23 | dplyr, 24 | fs, 25 | glue, 26 | purrr, 27 | readr, 28 | rlang, 29 | shiny, 30 | stringr, 31 | tibble, 32 | tidyr, 33 | utils 34 | Suggests: 35 | callr, 36 | covr, 37 | devtools, 38 | knitr, 39 | pkgload, 40 | rstudioapi, 41 | testthat (>= 3.0.0), 42 | tidyverse 43 | Encoding: UTF-8 44 | Roxygen: list(markdown = TRUE) 45 | RoxygenNote: 7.3.2 46 | Config/testthat/edition: 3 47 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(autoimport) 4 | export(import_review) 5 | export(update_importlist) 6 | importFrom(cli,cli_abort) 7 | importFrom(cli,cli_h1) 8 | importFrom(cli,cli_h2) 9 | importFrom(cli,cli_inform) 10 | importFrom(cli,cli_warn) 11 | importFrom(cli,format_inline) 12 | importFrom(cli,qty) 13 | importFrom(digest,digest) 14 | importFrom(dplyr,"%>%") 15 | importFrom(dplyr,arrange) 16 | importFrom(dplyr,as_tibble) 17 | importFrom(dplyr,bind_rows) 18 | importFrom(dplyr,desc) 19 | importFrom(dplyr,distinct) 20 | importFrom(dplyr,filter) 21 | importFrom(dplyr,lag) 22 | importFrom(dplyr,last) 23 | importFrom(dplyr,lead) 24 | importFrom(dplyr,left_join) 25 | importFrom(dplyr,mutate) 26 | importFrom(dplyr,pull) 27 | importFrom(dplyr,rename) 28 | importFrom(dplyr,rowwise) 29 | importFrom(dplyr,select) 30 | importFrom(dplyr,setdiff) 31 | importFrom(dplyr,starts_with) 32 | importFrom(dplyr,summarise) 33 | importFrom(dplyr,transmute) 34 | importFrom(dplyr,ungroup) 35 | importFrom(fs,dir_create) 36 | importFrom(fs,file_create) 37 | importFrom(fs,file_delete) 38 | importFrom(fs,file_exists) 39 | importFrom(fs,file_move) 40 | importFrom(fs,path) 41 | importFrom(fs,path_abs) 42 | importFrom(fs,path_dir) 43 | importFrom(fs,path_temp) 44 | importFrom(glue,glue) 45 | importFrom(glue,glue_data) 46 | importFrom(purrr,imap) 47 | importFrom(purrr,keep) 48 | importFrom(purrr,list_rbind) 49 | importFrom(purrr,map) 50 | importFrom(purrr,map2) 51 | importFrom(purrr,map2_chr) 52 | importFrom(purrr,map2_lgl) 53 | importFrom(purrr,map_chr) 54 | importFrom(purrr,map_dbl) 55 | importFrom(purrr,map_depth) 56 | importFrom(purrr,map_int) 57 | importFrom(purrr,map_lgl) 58 | importFrom(purrr,walk) 59 | importFrom(readr,read_lines) 60 | importFrom(readr,write_lines) 61 | importFrom(rlang,caller_arg) 62 | importFrom(rlang,check_dots_empty) 63 | importFrom(rlang,check_installed) 64 | importFrom(rlang,current_env) 65 | importFrom(rlang,hash) 66 | importFrom(rlang,hash_file) 67 | importFrom(rlang,is_installed) 68 | importFrom(rlang,ns_env) 69 | importFrom(rlang,set_names) 70 | importFrom(stringr,regex) 71 | importFrom(stringr,str_detect) 72 | importFrom(stringr,str_ends) 73 | importFrom(stringr,str_extract) 74 | importFrom(stringr,str_pad) 75 | importFrom(stringr,str_remove) 76 | importFrom(stringr,str_split_1) 77 | importFrom(stringr,str_squish) 78 | importFrom(stringr,str_starts) 79 | importFrom(stringr,str_subset) 80 | importFrom(tibble,deframe) 81 | importFrom(tibble,tibble) 82 | importFrom(tidyr,complete) 83 | importFrom(utils,capture.output) 84 | importFrom(utils,getParseData) 85 | importFrom(utils,getSrcref) 86 | importFrom(utils,menu) 87 | importFrom(utils,modifyList) 88 | importFrom(utils,sessionInfo) 89 | importFrom(utils,stack) 90 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | 2 | # autoimport 3 | 4 | `autoimport` is a package designed to to automatically generate @importFrom roxygen tags from R files. Browse code at . 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 | [![Package-License](http://img.shields.io/badge/license-GPL--3-brightgreen.svg?style=flat)](http://www.gnu.org/licenses/gpl-3.0.html) 6 | [![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) 7 | [![CRAN status](https://www.r-pkg.org/badges/version/autoimport)](https://CRAN.R-project.org/package=autoimport) 8 | [![Last Commit](https://img.shields.io/github/last-commit/DanChaltiel/autoimport)](https://github.com/DanChaltiel/autoimport) 9 | [![R-CMD-check](https://github.com/DanChaltiel/autoimport/actions/workflows/R-CMD-check.yaml/badge.svg)](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 | ![](inst/figures/showcase.gif) 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 | --------------------------------------------------------------------------------