Skip to content

[R-pkg-devel] R CMD check false positive unused imports inside R6 class

9 messages · Michael Chirico, Bjarke Hautop, Dirk Eddelbuettel +1 more

#
Hi all,

R CMD check gives a false positive locally when the only usage of an
imported package is through pkg::foo() inside an R6 class. This GitHub repo
contains a full MWE, with log files and a more elaborate explanation:
https://github.com/BjarkeHautop/RCMDcheckFalsePositive

The R package contains a single .R file with this (I'm aware you don't have
to import base packages explicitly, but this is an MWE with only base
packages (except R6)):

filter <- function() {
  message("This is a custom filter function.")
}

DataProcessor <- R6::R6Class(
  "DataProcessor",
  public = list(
    data = NULL,
    initialize = function(data) self$data <- data,

    filter_data = function(data) {
      filter()
      self$data <- stats::filter(self$data, rep(1, 3))
    }
  )
)

When running R CMD check, it will generate the following NOTE:

* checking dependencies in R code ... NOTE
Namespaces in Imports field not imported from:
  ?R6? ?stats?

Interestingly, the NOTE disappears on CRAN release/dev winbuilder.

My questions are:

1. Is this the intended behavior of R CMD check, or is it a bug that it
fails to detect usage of packages inside
R6 classes? If intended (e.g., due to it being too expensive to check for
:: in "hidden places") should this be
mentioned somewhere on WRE? Currently
[WRE](
https://cran.r-project.org/doc/manuals/r-devel/R-exts.html#Package-Dependencies-1)
says:
*"The ?Imports? field should not contain packages which are not imported
from (via the NAMESPACE file or :: or ::: operators)"*
indicating that `::` usage should be fully supported.

2. How/Why does the NOTE disappear when checking on CRAN dev winbuilder?
Can I replicate this behavior locally using R CMD check? Will it pass on
CRAN?

Best regards,
Bjarke
#
On 12 February 2026 at 15:52, Bjarke Hautop wrote:
| Hi all,
| 
| R CMD check gives a false positive locally when the only usage of an
| imported package is through pkg::foo() inside an R6 class. This GitHub repo
| contains a full MWE, with log files and a more elaborate explanation:
| https://github.com/BjarkeHautop/RCMDcheckFalsePositive
| 
| The R package contains a single .R file with this (I'm aware you don't have
| to import base packages explicitly, but this is an MWE with only base
| packages (except R6)):
| 
| filter <- function() {
|   message("This is a custom filter function.")
| }
| 
| DataProcessor <- R6::R6Class(
|   "DataProcessor",
|   public = list(
|     data = NULL,
|     initialize = function(data) self$data <- data,
| 
|     filter_data = function(data) {
|       filter()
|       self$data <- stats::filter(self$data, rep(1, 3))
|     }
|   )
| )
| 
| When running R CMD check, it will generate the following NOTE:
| 
| * checking dependencies in R code ... NOTE
| Namespaces in Imports field not imported from:
|   ?R6? ?stats?

Thanks for posting a full and complete example!  From a quick glance your
problem may be that while you DO have the the packages in DESCRIPTION you
DO NOT import them in NAMESPACE.  The error message could arguably be more
explicit but that seems to be the case here.

Hope this helps, Dirk
 
| Interestingly, the NOTE disappears on CRAN release/dev winbuilder.
| 
| My questions are:
| 
| 1. Is this the intended behavior of R CMD check, or is it a bug that it
| fails to detect usage of packages inside
| R6 classes? If intended (e.g., due to it being too expensive to check for
| :: in "hidden places") should this be
| mentioned somewhere on WRE? Currently
| [WRE](
| https://cran.r-project.org/doc/manuals/r-devel/R-exts.html#Package-Dependencies-1)
| says:
| *"The ?Imports? field should not contain packages which are not imported
| from (via the NAMESPACE file or :: or ::: operators)"*
| indicating that `::` usage should be fully supported.
| 
| 2. How/Why does the NOTE disappear when checking on CRAN dev winbuilder?
| Can I replicate this behavior locally using R CMD check? Will it pass on
| CRAN?
| 
| Best regards,
| Bjarke
| 
| 	[[alternative HTML version deleted]]
| 
| ______________________________________________
| R-package-devel at r-project.org mailing list
| https://stat.ethz.ch/mailman/listinfo/r-package-devel
#
R6 is a _build-time_ dependency for your package. It may be possible for a
user without R6 installed to run your package without issue from the built
tarball. R today doesn't really have a concept of a build-time dependency
in the DESCRIPTION

'stats' is more of a false positive -- {codetools} simply doesn't know how
to check the R6 object DataProcessor (which it sees as an environment and
doesn't walk completely). It could find 'stats::' if it looked at

body(getNamespace(<pkg>)$DataProcessor$public_methods$filter_data)[[3]][[3]][[1]]

Anyway, Dirk's advice is correct: you can just add the entries to your
NAMESPACE:

importFrom(R6, R6class)
importFrom(stats, filter)

Once that's done, you can choose whether to continue namespace-qualifying
the calls inside the sources.

You could also explore if just putting the packages in Suggests, not
Imports, works.

Mike C
On Thu, Feb 12, 2026 at 2:33?PM Dirk Eddelbuettel <edd at debian.org> wrote:

            

  
  
#
I suspect that's due to a difference of running the check on the built
package vs. on the package sources:

tools:::.check_packages_used(dir = ".") # nothing
tools:::.check_packages_used(package = <pkg>)
# Namespaces in Imports field not imported from:
#   ?R6? ?stats?
#   All declared Imports should be used.

Mike C

On Thu, Feb 12, 2026 at 2:53?PM Michael Chirico <michaelchirico4 at gmail.com>
wrote:

  
  
#
Thanks, Dirk and Michael,

In my opinion, there are several reasons why you wouldn't want to just do
importFrom(stats, filter). Using devtools::load_all() typing "filte" in the
console now only brings up Base::Filter and stats::filter and not the
function that will actually be called from filter(),
i.e., my internal filter function. Additionally, you can't do this if the
reason you did pkg::foo() was to avoid nameclash between two different
packages, e.g., extend my example with also dplyr::filter. You can't add

importFrom(stats, filter)
importFrom(dplyr, filter)

How I see it, the only safe way to do it in this case is to use
stats::filter and dplyr::filter outside the R6 class, either by refactoring
(which might make the code more unreadable) or adding an unused function,
such as this:

avoid_cran_note() {
  stats::filter(1:10, rep(1, 3))
  my_data <- data.frame(
    x = 1:5,
    y = c(10, 20, 30, 40, 50)
  )
  dplyr::filter(my_data, my_data$x > 3)
}

Neither approach seems ideal to me.

Best regards,
Bjarke



Den tors. 12. feb. 2026 kl. 23.53 skrev Michael Chirico <
michaelchirico4 at gmail.com>:

  
  
#
Sorry, clicked send too soon!
body(getNamespace(<pkg>)$DataProcessor$public_methods$filter_data)[[3]][[3]][[1]]

Is there a reason why it doesn't do this? Is it just for computational
reasons?

Best regards,
Bjarke

Den tors. 12. feb. 2026 kl. 23.53 skrev Michael Chirico <
michaelchirico4 at gmail.com>:

  
  
#
I reckon it was just written before R6 and could go for a patch:
https://gitlab.com/luke-tierney/codetools

Point taken about masking. Working from memory (CMIIW) you can just import
and name from stats to satisfy the static analysis:

importFrom(stats, setNames)

R doesn't do two-way "include what you use" checks to insure the names you
import are actually used.

Mike C

On Thu, Feb 12, 2026, 11:33?PM Bjarke Hautop <bjarke.hautop at gmail.com>
wrote:

  
  
#
Bjarke,

I had sent you a minimal diff that silenced R CMD check after you replied to
me off-list; if it were my package I'd stop there.

But you can of course continue to insist that you should be allowed to have a
local, internal filtering function -- and "yes you can". But the common
(documented) trick is to use _an internal function_ differentiated with a
leading dot, i.e. `.filter()`.  So with the diff below I once again only have
a warning from your class being undocumented. That R6 needs to be imported
seems par for the course; Michael rightly suggested that you could patch
`codetools` if it really bugs you.

Cheers, Dirk

edd at paul:/tmp/r/RCMDcheckFalsePositive(main)$ git diff
diff --git i/NAMESPACE w/NAMESPACE
index dbd6067..817a7ee 100644
--- i/NAMESPACE
+++ w/NAMESPACE
@@ -1 +1,2 @@
 export(DataProcessor)
+import(R6)
diff --git i/R/hello.R w/R/hello.R
index a608963..85befc4 100644
--- i/R/hello.R
+++ w/R/hello.R
@@ -1,4 +1,4 @@
-filter <- function() {
+.filter <- function() {
   message("This is a custom filter function.")
 }
 
@@ -9,7 +9,7 @@ DataProcessor <- R6::R6Class(
     initialize = function(data) self$data <- data,
 
     filter_data = function(data) {
-      filter()
+      .filter()
       self$data <- stats::filter(self$data, rep(1, 3))
     }
   )
edd at paul:/tmp/r/RCMDcheckFalsePositive(main)$
#
? Thu, 12 Feb 2026 14:57:59 -0800
Michael Chirico <michaelchirico4 at gmail.com> ?????:
I wonder what happens on Win-Builder during the check. According to the
code (tools/R/check.R line 2156, function check_R_code),
tools:::.check_packages_used(dir = ...) is only used if !do_install,
and 00check.log from Win-Builder doesn't seem to divulge anything that
would set do_install to FALSE. (Checking with --install=fake or
--install=check:... still sets do_install to TRUE and causes the NOTE).
The function is quite self-contained; indeed, it only walks LANGSXP
call objects, expression vectors, and pairlists made from functions.

Does tools:::.check_packages_used(package=...) really fail to notice
the "unused" imports on Win-Builder? Does something unseen really set
do_install <- FALSE? Is there a third reason?