Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ img
^CRAN-SUBMISSION$
^attachment\.Rproj$
^CONTRIBUTING\.md$
^CLAUDE\.md$
48 changes: 48 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this package is

`{attachment}` is an R package that manages dependencies during R package development: it parses NAMESPACE, `R/`, `vignettes/`, tests, and roxygen examples to discover package calls (`library()`, `pkg::fun()`, `@importFrom`) and rewrites the `Imports` / `Suggests` fields of `DESCRIPTION`. It also offers dependency install helpers and `renv` integration. It can also be used outside of a package to list dependencies of arbitrary R scripts or Rmd/qmd files.

## Common commands

Run from the package root in an R session (the package is developed with `devtools` / `usethis`; `{fusen}` was used historically but its workflow has been removed):

- `devtools::load_all()` — load the in-development package.
- `devtools::document()` — regenerate `NAMESPACE` and `man/*.Rd` from roxygen.
- `devtools::test()` — run the full test suite (`tests/testthat/`).
- `testthat::test_file("tests/testthat/test-amend-description.R")` — run a single test file. The tests in `test-amend-description.R` and `test-renv_create.R` are gated on `NOT_CRAN`; to run them locally wrap with `withr::with_envvar(list("NOT_CRAN" = "true"), { ... })` as in `dev/dev_history.R`.
- `devtools::check()` — full R CMD check (also run by CI via `.github/workflows/R-CMD-check.yaml`).
- `devtools::build_vignettes()` — builds the four vignettes in `vignettes/`.
- `attachment::att_amend_desc()` — the package's own dog-food step to refresh `DESCRIPTION`; parameters are read from `dev/config_attachment.yaml` (edit that file rather than retyping arguments).

There is no `Makefile`; `dev/dev_history.R` is the canonical scratchpad of maintainer commands (dependency refresh, CRAN prep, reverse-dep checks).

## Architecture

### Core function layers

There are three layers, roughly:

1. **Extractors** — pure functions that parse one source and return a character vector of package names. One file per source type: `att_from_namespace.R`, `att_from_rmds.R` (Rmd + qmd), `att_from_rscripts.R`, `att_from_examples.R`, `att_from_data.R`, `att_from_description.R`. These can be called standalone, outside a package.
2. **DESCRIPTION writers** — `att_to_description.R` (despite the filename, this is where `att_amend_desc()` / `att_to_desc_from_pkg()` / `att_to_desc_from_is()` live — this is the biggest file in the package at ~450 lines and orchestrates all the extractors) and `set_remotes.R` (fills `Remotes:` by inspecting locally-installed packages via `find_remotes()`).
3. **Config, install, renv helpers** — `amend_with_config.R` (load/save/compare `dev/config_attachment.yaml`), `install_from_description.R`, `create_dependencies_file.R` + `dependencies_file_text.R` (emit `inst/dependencies.R`), `create_renv.R` (generate dev/prod `renv` lockfiles).

The typical user entry point is `att_amend_desc()`: it calls extractors on each directory, unions the results, classifies into `Imports` vs `Suggests` based on which directory the package was found in, and rewrites `DESCRIPTION` via the `{desc}` package.

### Config-driven parameter loading

`att_amend_desc()` has many arguments (ignore lists, extra suggests, dir overrides). `amend_with_config.R` implements a three-way reconcile between (a) arguments passed by the user, (b) previously-saved values in `dev/config_attachment.yaml`, and (c) function defaults. If the user passes non-default args that also differ from the saved config, it errors and asks the user to pick `update.config = TRUE` or `use.config = FALSE`. When adding a new argument to `att_amend_desc()`, you must also: add it to the `local_att_params` list inside the function, and make sure `save_att_params()`/`load_att_params()` round-trip it.

### Test fixtures

`inst/dummypackage/` is a minimal fake R package used as the fixture for most tests (and README examples). Tests copy it into `tempfile()` before mutating it — preserve that pattern for new tests so they stay hermetic. There is also `inst/dummyfolder/` for non-package script tests, and a collection of `f*.R` / `*.Rmd` / `*.qmd` files under `tests/testthat/` that are raw parse targets for the extractors (not test scripts themselves — they have no `test_that`).

## Conventions

- Tidyverse style guide, roxygen2 with markdown (`Roxygen: list(markdown = TRUE)` in `DESCRIPTION`).
- User-facing changes go in a new bullet at the top of `NEWS.md` under the current development version header.
- New exports must be declared via `@export` roxygen tags; `NAMESPACE` is regenerated, not hand-edited.
- `dev/` is `.Rbuildignore`d — it is developer-only and not shipped to CRAN.
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: attachment
Title: Deal with Dependencies
Version: 0.4.5.0001
Version: 1.0.0
Authors@R: c(
person("Vincent", "Guyader", , "vincent@thinkr.fr", role = c("cre", "aut"),
comment = c(ORCID = "0000-0003-0671-9270")),
Expand Down Expand Up @@ -30,7 +30,7 @@ Config/Needs/website: ThinkR-open/thinkrtemplate
Config/testthat/edition: 3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
RoxygenNote: 7.3.3
Language: en-US
Depends:
R (>= 3.4)
Expand Down
66 changes: 63 additions & 3 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,66 @@
# dev version

- fix `create_renv_for_dev` by removing 'renv' from folder_to_include.
# attachment 1.0.0

## Detection — new foundation

- `att_from_rscript()` now walks the R syntax tree instead of matching the
source text with regexes. Dependency detection no longer produces false
positives on `::` inside string literals or comments (e.g. xpath
`following-sibling::td`, CSS `label::after`, `sprintf('%s::plot()', ...)`)
(#120, #132).
- The AST walker safely handles "empty" call arguments (`x[, 1]`, `x[1, ]`,
empty `switch` alternatives, missing `else` branches). Earlier development
drafts of the walker crashed with `argument "el" is missing, with no
default` on any script containing these patterns, which would have
silently degraded every real-world script to the legacy regex fallback.

## Detection — new patterns recognised

- `att_from_rscript()` also recognises `use("pkg", ...)` (R >= 4.4) (#128),
`getFromNamespace("fn", "pkg")`, `loadNamespace()`, and named-argument
forms such as `library(package = "pkg")` or
`requireNamespace("pkg", lib.loc = "...")`.
- Fully-qualified dependency-introducing calls such as `base::library(pkg)`,
`base::requireNamespace("pkg")`, and `methods::getFromNamespace(fn, "pkg")`
are now honoured; the inner package is added to the dependency list.
- Introspection helpers (`packageVersion()`, `getNamespace()`,
`asNamespace()`, `attachNamespace()`) are intentionally **not** treated as
dependency introducers to avoid silently widening `Imports` on code that
only uses them for feature detection.

## Detection — robustness

- `att_from_rscript()` gains an `encoding` argument (default
`getOption("encoding")`) so scripts saved in Latin-1 / Windows-1252 are
read with the system locale instead of being forced to UTF-8.
- `att_from_rscript()` now warns when a file fails to parse as valid R code
and the legacy regex-based detector is used as a fallback, so broken
scripts are no longer silently degraded.
- The legacy regex-based fallback detector now accepts underscores in
package names (e.g. `my_pkg::fn`). Previously an underscore truncated the
detected name to the portion after the underscore, so a single syntax
error upstream could corrupt detection across the whole file.

## Vignettes & Rmd / Quarto

- `att_from_rmds()` infers the vignette engine from the files actually
present: `quarto` is added when `.qmd` files are found, `rmarkdown` when
`.Rmd` files are found, both when the directory mixes the two.
Previously `rmarkdown` was forced in for `.qmd`-only projects (#131).
- `att_from_rmd()` / `att_from_rmds()`: `inside_rmd` now defaults to `NULL`
and is auto-detected via `knitr::opts_knit$get("out.format")`, so users
no longer have to think about it (#106).

## Maintenance

- Dropped the `{fusen}` development workflow; `R/amend_with_config.R`,
`R/create_dependencies_file.R`, `R/dependencies_file_text.R` and their
tests are now hand-maintained.
- Fixed `create_renv_for_dev()` by removing `'renv'` from
`folder_to_include`.
- Added a manual detection edge-case harness at
`dev/manual_detection_edge_cases.R` covering ~170 scripted cases (true
positives, false positives, known limitations) to make future
regressions of the detector obvious.


# attachment 0.4.5
Expand Down
1 change: 0 additions & 1 deletion R/amend_with_config.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# WARNING - Generated by {fusen} from dev/flat_save_att_params.Rmd: do not edit by hand

#' Compare input from the user to config file or default inputs
#'
Expand Down
29 changes: 22 additions & 7 deletions R/att_from_rmds.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#' @param path Path to a Rmd file
#' @param temp_dir Path to temporary script from purl vignette
#' @param warn -1 for quiet warnings with purl, 0 to see warnings
#' @param inside_rmd Logical. Whether function is run inside a Rmd,
#' in case this must be executed in an external R session
#' @param inside_rmd Logical or `NULL`. Whether the function is being called
#' from inside a knit session, in which case the actual purl step must be
#' delegated to an external R process. When `NULL` (the default), this is
#' auto-detected via `knitr::opts_knit$get("out.format")`.
#' @param inline Logical. Default TRUE. Whether to explore inline code for dependencies.
#' @inheritParams knitr::purl
#'
Expand All @@ -22,9 +24,13 @@
#' @export
att_from_rmd <- function(path, temp_dir = tempdir(), warn = -1,
encoding = getOption("encoding"),
inside_rmd = FALSE, inline = TRUE) {
inside_rmd = NULL, inline = TRUE) {
if (missing(path)) {stop("argument 'path' is missing, with no default")}

if (is.null(inside_rmd)) {
inside_rmd <- !is.null(knitr::opts_knit$get("out.format"))
}

op <- options(knitr.purl.inline = inline)
on.exit(options(op))

Expand Down Expand Up @@ -83,8 +89,11 @@ att_from_rmd <- function(path, temp_dir = tempdir(), warn = -1,
#' @inheritParams att_from_rmd
#'
#' @return Character vector of packages called with library or require.
#' \emph{knitr} and \emph{rmarkdown} are added by default to allow building the vignettes
#' if the directory contains "vignettes" in the path
#' When the directory contains "vignettes" in its path, `knitr` is always added
#' and the vignette engine is inferred from the files actually present:
#' `rmarkdown` is added when `.Rmd` files are found, `quarto` when `.qmd`
#' files are found (both when the directory mixes the two). If the directory
#' is empty, `rmarkdown` is added as a safe default.
#'
#' @examples
#' dummypackage <- system.file("dummypackage",package = "attachment")
Expand All @@ -95,7 +104,7 @@ att_from_rmd <- function(path, temp_dir = tempdir(), warn = -1,
att_from_rmds <- function(path = "vignettes",
pattern = "*.[.](Rmd|rmd|qmd)$",
recursive = TRUE, warn = -1,
inside_rmd = FALSE, inline = TRUE,folder_to_exclude = "renv") {
inside_rmd = NULL, inline = TRUE,folder_to_exclude = "renv") {

if (isTRUE(all(dir.exists(path)))) {
all_f <- list.files(path, full.names = TRUE, pattern = pattern, recursive = recursive)
Expand Down Expand Up @@ -126,7 +135,13 @@ att_from_rmds <- function(path = "vignettes",
na.omit()

if (isTRUE(any(grepl("vignettes", path)))) {
unique(c("knitr", "rmarkdown", res))
engine <- character(0)
if (length(all_f)) {
if (any(grepl("\\.qmd$", all_f, ignore.case = TRUE))) engine <- c(engine, "quarto")
if (any(grepl("\\.rmd$", all_f, ignore.case = TRUE))) engine <- c(engine, "rmarkdown")
}
if (length(engine) == 0) engine <- "rmarkdown"
unique(c("knitr", engine, res))
} else {
res
}
Expand Down
Loading
Loading