Skip to content

New rule: explicit package names #474

Description

@christopherkenny

When shifting from an analysis script to package code, it is often helpful to turn:

library(dplyr)
library(ggplot2)

iris |>
  mutate(something = seq_len(nrow(iris)) |>
  ggplot() +
  geom_point(...)

into:

library(dplyr)
library(ggplot2)

iris |>
  dplyr::mutate(something = seq_len(nrow(iris)) |>
  ggplot2::ggplot() +
  ggplot2::geom_point(...)

to allow easy copy-pasting into the package.

It would be great to have a rule to make package names explicit in this way. I've given it a go, since prototypes are very cheap now. It implements this with a few specific choices to make it consistent:

  1. If a function has multiple package sources, don't try to resolve it.
  2. If a function has one package source, but is re-exported (e.g., tibble()), then resolve to the original (tibble() -> tibble, not to dplyr if both are loaded. Goes to dplyr if tibble is not loaded.)
  3. If a function has a local source, treat that as the truth.

Proof of concept

Input:

library(dplyr)
library(tibble)
library(rlang)

# Clear dplyr calls should be flagged and fixed.
x <- tibble(a = 1:5, g = c("a", "a", "b", "b", "b"))
y <- mutate(x, b = a + 1)
z <- summarise(group_by(y, g), total = sum(b))

# Re-exported calls should resolve to the provider package, not the re-exporter.
nested <- tibble(
  id = 1:3,
  label = as_label(expr(id))
)

# Already explicit calls should be left alone.
explicit <- dplyr::mutate(x, c = a * 2)

# Default R packages should be left alone.
base_calls <- list(mean(x$a), median(x$a), sum(x$a))

# This is ambiguous between stats and dplyr, so the rule should report without a fix.
maybe_ambiguous <- filter(x, a > 2)

# Local definitions should be left alone even if a package exports the same name.
tibble <- function(value) {
  value
}
local_shadow <- tibble("not a package call")

# Calls with internal comments should preserve comments when fixed.
with_comment <- mutate(
  x,
  # keep this comment in place
  d = a + 10
)

Output, with fixing:

library(dplyr)
library(tibble)
library(rlang)

# Clear dplyr calls should be flagged and fixed.
x <- tibble::tibble(a = 1:5, g = c("a", "a", "b", "b", "b"))
y <- dplyr::mutate(x, b = a + 1)
z <- dplyr::summarise(dplyr::group_by(y, g), total = sum(b))

# Re-exported calls should resolve to the provider package, not the re-exporter.
nested <- tibble::tibble(
  id = 1:3,
  label = rlang::as_label(rlang::expr(id))
)

# Already explicit calls should be left alone.
explicit <- dplyr::mutate(x, c = a * 2)

# Default R packages should be left alone.
base_calls <- list(mean(x$a), median(x$a), sum(x$a))

# This is ambiguous between stats and dplyr, so the rule should report without a fix.
maybe_ambiguous <- filter(x, a > 2)

# Local definitions should be left alone even if a package exports the same name.
tibble <- function(value) {
  value
}
local_shadow <- tibble("not a package call")

# Calls with internal comments should preserve comments when fixed.
with_comment <- dplyr::mutate(
  x,
  # keep this comment in place
  d = a + 10
)

Console notes:

warning: explicit_packages
  --> explicit_packages_stretch.R:23:20
   |
23 | maybe_ambiguous <- filter(x, a > 2)
   |                    ------ Cannot choose an explicit package qualifier for `filter()`.
   |
   = help: `filter()` is exported by multiple loaded packages: stats, dplyr.

Metadata

Metadata

Assignees

No one assigned

    Labels

    futurenew-ruleSuggest the implementation of a new rule in Jarl.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions