Skip to content

feat(linter/node): implement no-top-level-await rule#24634

Merged
camc314 merged 8 commits into
mainfrom
feat/node-no-top-level-await
Jul 19, 2026
Merged

feat(linter/node): implement no-top-level-await rule#24634
camc314 merged 8 commits into
mainfrom
feat/node-no-top-level-await

Conversation

@connorshea

@connorshea connorshea commented Jul 18, 2026

Copy link
Copy Markdown
Member

This implements no-top-level-await from the Node ESLint plugin (#493).

AI Disclosure: Generated with Claude Code. Reviewed and tested by me.

I have an alternative implementation using a Program visitor but it made no difference performance-wise on vscode or in benchmarking outside extreme cases, so I don't think it's really worth the trouble vs the simpler implementation here. It's still in the commit history if that implementation is preferred.

There is one notable difference between this and the original implementation, which is that the convertPath option is not implemented, and the checking of package.json/.npmignore is not implemented (it was used for checking if the given module was actually exposed in the library) as there's no way to check against those in oxlint. There's an argument to be made that this makes the rule not worth implementing, and I'm open to that argument if we'd prefer to mark this rule as unsupported instead.

All tests were ported from the original rule (excluding those reliant on the mentioned config options/checks that were skipped). I've also had Claude add a few additional test cases (see commit 40418f7) on top to cover behavior that wasn't tested previously. I have confirmed that all of these tests are identical to the behavior of the upstream rule, but I can remove them if we don't want to have any divergence from the upstream test suite.

connorshea and others added 6 commits July 17, 2026 20:26
Port of eslint-plugin-n's `no-top-level-await`. Disallows top-level
`await`, `for await...of`, and `await using` outside of any function,
since ES modules with top-level `await` cannot be loaded via `require(esm)`.

Supports the portable `ignoreBin` option (skips files starting with a
hashbang). The filesystem-dependent parts of the original rule
(package.json/.npmignore resolution, bin field, convertPath) are omitted
as they have no counterpart in oxc's linter.

AI usage disclosure: implemented with Claude Code.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add cases for the rule's distinct branches: plain `using` (not flagged),
`await` in class/object methods and nested non-async functions, bare
top-level `await` statements, `await` nested in non-function blocks/loops,
and `await using` with `ignoreBin` + hashbang.

AI usage disclosure: implemented with Claude Code.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Subscribe the rule to `Program` only and walk top-level code with a
visitor that stops descending at function boundaries, instead of
subscribing to every `AwaitExpression`/`ForOfStatement`/`VariableDeclaration`
node and walking ancestors to check for an enclosing function.

Most `await`/`for-of`/`var` nodes live inside functions, so the old
approach paid per-node dispatch on the ubiquitous node types only to
reject them via an ancestor walk. The visitor visits just the slice of
code that can hold a top-level `await`.

Behavior is byte-identical (verified against a 100K-line fixture with
~88K in-function nodes: same diagnostics). On that worst case the rule's
own overhead drops ~0.8ms (95% CI 0.4-1.1ms, paired n=120).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The `ignoreBin` option docs are generated automatically from the config
struct's field doc comment, so the hand-written `### Options` block is
redundant.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@github-actions github-actions Bot added A-linter Area - Linter A-cli Area - CLI labels Jul 18, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 18, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 71 skipped benchmarks1


Comparing feat/node-no-top-level-await (0236018) with main (d76d899)

Open in CodSpeed

Footnotes

  1. 71 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

The config holds only one `bool`, so `Box<NoTopLevelAwaitConfig>` added a
heap allocation and pointer indirection per rule instance with no size or
heap-content justification (unlike sibling rules that box an FxHashSet).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@connorshea
connorshea marked this pull request as ready for review July 18, 2026 02:58
@connorshea
connorshea requested a review from camc314 as a code owner July 18, 2026 02:58
Comment thread crates/oxc_linter/src/rules/node/no_top_level_await.rs

@camc314 camc314 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@camc314 camc314 changed the title feat(linter): Implement node/no-top-level-await rule feat(linter/node): implement no-top-level-await rule Jul 19, 2026
@camc314 camc314 self-assigned this Jul 19, 2026
@camc314
camc314 merged commit dd18383 into main Jul 19, 2026
31 checks passed
@camc314
camc314 deleted the feat/node-no-top-level-await branch July 19, 2026 13:10
camc314 added a commit that referenced this pull request Jul 21, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 54cc121 ast: [**BREAKING**] Split `MetaProperty` into `ImportMeta` and
`NewTarget` (#24557) (camc314)

### 🚀 Features

- 7b045cd minfier: Drop last break from last switch case (#24673)
(Armano)
- dd18383 linter/node: Implement no-top-level-await rule (#24634)
(Connor Shea)
- 16a65f2 linter/react: Implement function-component-definition rule
(#24471) (Cole Ellison)
- 7f1f585 linter: Reuse `jest/padding-around-test-blocks` for
`vitest/padding-around-test-blocks` (#24519) (Mikhail Baev)
- 99978a8 linter/import/consistent-type-specifier-style: Support
`prefer-top-level-if-only-type-imports` option (#24502) (camc314)

### 🐛 Bug Fixes

- 0184ad6 linter/unicorn/no-useless-undefined: Preserve valid parameter
defaults (#24686) (camc314)
- 8694167 linter/eslint/prefer-destructuring: Handle typed declarations
(#24616) (camc314)
- 477cf0f linter/eslint/no-throw-literal: Handle assigned errors
(#24561) (Cole Ellison)
- ac9200a linter: Detect React components from returned JSX (#24521)
(camc314)
- c0a6522 linter/eslint/no-useless-computed-key: Allow TS syntax in
computed keys (#24524) (Cole Ellison)

### ⚡ Performance

- 346eed1 linter/unicorn/prefer-event-target: Only run on `Class` and
`NewExpression` nodes (#24685) (Mikhail Baev)
- 7be5cf0 oxlint/lsp: Only invoke lint on code actions when document is
not opened (#24676) (Sysix)
- d3f07a0 diagnostics: Box OxcDiagnosticInner to reduce binary size
(#24665) (Boshen)
- 90ae040 linter/reporter/stylish: Compute diagnostic Info once per
diagnostic (#24525) (connorshea)

### 📚 Documentation

- e6f7174 linter/valid-expect: Fix correct example being identical to
incorrect one (#24468) (mkan0141)
# Oxfmt
### 💥 BREAKING CHANGES

- 54cc121 ast: [**BREAKING**] Split `MetaProperty` into `ImportMeta` and
`NewTarget` (#24557) (camc314)

### 🚀 Features

- 3d22307 parser: Add `ParseOptions::enable_ident_hashes` (#24491)
(Boshen)

### 🐛 Bug Fixes

- 6fe866a oxfmt: Keep tailwind classes glued to template expr with
`preserveWhitespace` (#24609) (leaysgur)
- 33e32d8 formatter_css: Use `line_suffix` for EOL line comment (#24580)
(leaysgur)
- 5f76998 formatter_graphql: Keep same line comments pending across
intervening tokens (#24579) (leaysgur)

Co-authored-by: Boshen <[email protected]>
Co-authored-by: Cameron <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-linter Area - Linter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants