Skip to content

[New] add requireResolve option to check require.resolve() paths#3264

Open
manzoorwanijk wants to merge 7 commits into
import-js:mainfrom
manzoorwanijk:feat/require-resolve
Open

[New] add requireResolve option to check require.resolve() paths#3264
manzoorwanijk wants to merge 7 commits into
import-js:mainfrom
manzoorwanijk:feat/require-resolve

Conversation

@manzoorwanijk

@manzoorwanijk manzoorwanijk commented Jun 30, 2026

Copy link
Copy Markdown

Closes #585. Supersedes #1217.

Motivation

While working on a PR in Gutenberg (which powers the WordPress block editor), I noticed that require.resolve() calls are not considered by these rules. Looking for existing discussion I found #585, so this PR implements it.

Summary

require.resolve() resolves a module path the same way require() does, but until now the resolution rules ignored it. This PR adds an opt-in requireResolve boolean option (default false) to the shared moduleVisitor. When enabled, the first string-literal argument of a require.resolve("<path>") call is visited as a require module path, so the rules that resolve specifiers can check it too.

/*eslint import/no-unresolved: [2, { requireResolve: true }]*/
const x = require.resolve('./foo'); // reported if './foo' is not found
require.resolve('./foo', { paths: [__dirname] }); // first argument still checked

require.resolve(0); // ignored (non-string)
require['resolve']('./foo'); // ignored (computed access)

Relation to #1217

This supersedes the earlier #1217, which also added a requireResolve option but stalled in review. The main differences here: the option is a plain boolean independent of commonjs (instead of the boolean | { commonjs } oneOf), the first argument is checked regardless of arity (per @ljharb's review feedback on that PR), no-cycle explicitly ignores require.resolve, and the option is wired into several additional resolution rules — all with tests and docs.

Why opt-in (off by default)

This mirrors the conclusion of the earlier attempts (#1216 / #1217): emitting new warnings on existing code is a breaking change, and require.resolve is sometimes used to resolve non-module assets (e.g. build-generated files) that a resolver can't find. The option therefore defaults to false and is independent of commonjs.

Note on require.resolve(0): like require() and import(), the visitor only forwards static string paths, so non-string / dynamic arguments are skipped rather than reported. (In the #1217 review @ljharb noted a warning there could be useful; that is arguably a separate "invalid argument type" concern and is intentionally left out of scope here.)

Affected rules

The change lives in moduleVisitor, so every resolution rule can opt in:

  • Enabled via the shared options schema (no rule change needed): [no-unresolved], [no-absolute-path], [no-relative-packages], [no-relative-parent-imports].
  • [no-cycle]: explicitly ignores require.resolve — it computes a path without loading the module, so it cannot form a runtime cycle. This prevents false cycle reports when the option is enabled elsewhere.
  • Explicitly wired (these have their own schema and/or hardcode the visitor options): [no-extraneous-dependencies], [no-self-import], [no-webpack-loader-syntax], [no-useless-path-segments], [no-internal-modules].

Deliberately not wired: max-dependencies (a resolved path is not a load-time dependency edge), extensions (require.resolve is often used specifically to resolve paths with an extension), and no-nodejs-modules (marginal value).

Behaviour / edge cases

  • Arity is not restricted: the first argument is checked even in the 2-arg require.resolve(path, { paths }) form (per @ljharb's Add requireResolve option #1217 review).
  • Only non-computed require.resolve(...) member calls match; require['resolve'](...), require[resolve](...), and foo.require.resolve(...) are ignored.
  • Non-string / dynamic first arguments are ignored.
  • The visitor reports with moduleSystem: 'require', so resolver moduleSystem configuration applies consistently with require().

Checklist

  • write tests — valid (off-by-default) and invalid (option-on) cases for every wired rule, across the existing parser matrix where applicable
  • implement feature
  • update docs (docs/rules/*)
  • make a note in the change log

Testing

  • npm run tests-only for the affected suites — all passing
  • eslint ., tsc --noEmit index.d.ts, npm run update:eslint-docs -- --check, and markdownlint all clean

AI tool disclosure

This change was prepared with AI assistance. The plan, implementation, tests, and docs were drafted with Claude Code, and the plan and final diff were independently reviewed with Codex. All output was reviewed by me before submission, and I take responsibility for the contents of this PR.

…ve()` paths

Adds an opt-in `requireResolve` boolean (default false) to the shared module
visitor and its options schema. When enabled, single- or multi-arg
`require.resolve("<literal>")` calls have their first string argument visited as
a `require` module path, so resolution-based rules can check it.
…solve`

Resolves import-js#585. The shared `requireResolve` option (off by default) makes
`no-unresolved` report unresolvable `require.resolve("./foo")` paths.
`require.resolve` computes a path without loading the module, so it cannot form
a runtime dependency cycle. Skip it in the visitor so enabling `requireResolve`
does not produce false cycle reports.
…ia `requireResolve`

Adds the `requireResolve` option to the rule schema and forwards it to the
module visitor, so a package pulled in via `require.resolve("pkg")` is flagged
when it is not a declared dependency.
…segments`, `no-internal-modules`: check `require.resolve()` paths via `requireResolve`

Wires the opt-in `requireResolve` option into the remaining path-checking rules
that resolve module specifiers, so each also inspects `require.resolve()` paths.
@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.36%. Comparing base (99d5a50) to head (e298b1f).

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #3264       +/-   ##
===========================================
+ Coverage   80.95%   91.36%   +10.40%     
===========================================
  Files          97       85       -12     
  Lines        4438     3753      -685     
  Branches     1535     1369      -166     
===========================================
- Hits         3593     3429      -164     
+ Misses        845      324      -521     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@manzoorwanijk

Copy link
Copy Markdown
Author

@ljharb, a gentle request: is it possible to wrap this up before the next release?

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Support for checking require.resolve()

1 participant