Skip to content

fix(unused-deps): credit node:module register() loader specifiers (#293)#296

Merged
BartWaardenburg merged 1 commit intofallow-rs:mainfrom
ChrisJr404:fix/register-pattern-unused-deps-293
May 6, 2026
Merged

fix(unused-deps): credit node:module register() loader specifiers (#293)#296
BartWaardenburg merged 1 commit intofallow-rs:mainfrom
ChrisJr404:fix/register-pattern-unused-deps-293

Conversation

@ChrisJr404
Copy link
Copy Markdown
Contributor

Closes #293.

Problem

The visitor currently treats packages loaded via Node's register hook as unused. The OP's repro:

// resources/loaders/ts.js
import { register } from 'node:module';
import { pathToFileURL } from 'node:url';

register('@swc-node/register/esm', pathToFileURL('./'));
// package.json
{
  "devDependencies": { "@swc-node/register": "1.11.1" },
  "scripts": { "test-script": "node --import ./resources/loaders/ts.js ./test-script.ts" }
}

fallow dead-code flags @swc-node/register as unused even though it is loaded by specifier at runtime.

Fix

When a register(...) call is bound to the register export of node:module (or the unprefixed module), record the first-argument specifier as a DynamicImportInfo. The graph layer already credits the resolving package via ResolveResult::NpmPackage, so @swc-node/register/esm flows back to @swc-node/register in package.json.

Recognised forms:

// named
import { register } from 'node:module';
register('@swc-node/register/esm', pathToFileURL('./'));

// aliased
import { register as registerLoader } from 'node:module';
registerLoader('tsx/esm', import.meta.url);

// namespace
import * as Module from 'node:module';
Module.register('@swc-node/register/esm', import.meta.url);

Unrelated register(...) functions imported from anything other than node:module are not affected. Non-string first arguments (register(loaderUrl, ...)) are also ignored, matching how the visitor handles import(variable).

Tests

  • 7 new unit tests in crates/extract/src/tests/js_ts/dynamic_imports.rs covering named + aliased + namespace forms, the unprefixed module specifier, template-literal first-argument, the negative case (register from another module is not credited), and the dynamic-first-argument case (no entry recorded).
  • 1 new integration test in crates/core/tests/integration_test/false_positive_fixes.rs reproducing the OP's setup end-to-end and asserting @swc-node/register is not in unused_dev_dependencies.

Full suites green locally:

  • cargo test -p fallow-extract --lib — 1399 passed.
  • cargo test -p fallow-core --test integration_test false_positive_fixes — 16 passed.
  • cargo test -p fallow-cli --bin fallow — 1755 passed.

Notes

  • Imports are recorded during the same AST walk before the call is visited, and ESM hoists imports to the top of the module, so the visit-time check is sufficient for real-world code (mirrors how vi.mock and customElements.define are handled).
  • Aligned with the existing vitest_mock_source / extract_custom_elements_define style: a small free helper for the specifier extraction plus a method on ModuleInfoExtractor for the import-binding lookup.

Closes fallow-rs#293.

`register('@swc-node/register/esm', pathToFileURL('./'))` from
`node:module` loads a loader module by specifier rather than via a
static or dynamic `import`. Without recognising this hook the loader
package is reported as an unused (dev-)dependency.

The visitor now records the first argument of `register(...)` as a
`DynamicImportInfo` when the callee resolves to the `register` export
of `node:module` (or `module`). Both forms are matched:

  import { register } from 'node:module';
  register('@swc-node/register/esm', pathToFileURL('./'));

  import * as Module from 'node:module';
  Module.register('tsx/esm', import.meta.url);

Aliased named imports (`register as registerLoader`) work too. Calls to
unrelated `register(...)` functions are left untouched.
@BartWaardenburg BartWaardenburg force-pushed the fix/register-pattern-unused-deps-293 branch from 54ecfef to 0aa047c Compare May 6, 2026 13:11
@BartWaardenburg BartWaardenburg merged commit 12072d6 into fallow-rs:main May 6, 2026
17 checks passed
BartWaardenburg added a commit that referenced this pull request May 6, 2026
Reflect the broader implementation that landed in #296: vehicle
switched from require_calls to dynamic_imports, namespace-import
and template-literal first-arg forms now engage, helper renamed.
@BartWaardenburg
Copy link
Copy Markdown
Collaborator

Thanks @ChrisJr404, this is a nicer take than my earlier patch — the namespace-call shape (import * as Module from 'node:module'; Module.register(...)) and the no-substitution template-literal form weren't covered before, and your is_node_module_register helper with the via_namespace flag is cleaner. Shipped as merged.

@BartWaardenburg
Copy link
Copy Markdown
Collaborator

Released in v2.66.1. Thanks @ChrisJr404.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

take register into account for unused-dependencies or unused dev-dependencies

2 participants