Visitar URL original
fix: filter out reserved words from `exp.names` by danielroe · Pull Request #504 · unjs/unimport · GitHub
Skip to content

fix: filter out reserved words from exp.names - #504

Merged
antfu merged 2 commits into
mainfrom
fix/reserved-keywords
Mar 5, 2026
Merged

antfu merged 2 commits into
mainfrom
fix/reserved-keywords

Conversation

@danielroe

@danielroe danielroe commented Mar 4, 2026 •

Copy link
Copy Markdown
Member

latest unimport causes a regression due to relying on mlly's exp.names (see unjs/mlly#303)

I've raised unjs/mlly#336 to fix this at source but in the mean time might be worth this additional defensive check to avoid trying to auto-import reserved keywords

Summary by CodeRabbit

  • Bug Fixes

    • Improved export scanning logic to filter out JavaScript reserved words, preventing misidentification of export names and enhancing parsing accuracy.
  • Tests

    • Added test coverage for reserved word filtering to ensure correct handling of exports containing keyword-like patterns.

@danielroe
danielroe requested a review from antfu March 4, 2026 20:09
@coderabbitai

coderabbitai Bot commented Mar 4, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR introduces a regression test export (useSleep) and adds reserved word filtering to the export scanner. A new JS_RESERVED_WORDS set is implemented in scan-dirs to prevent JavaScript keywords from being incorrectly captured as export names during async arrow function parsing. Tests are updated to verify the filtering works correctly.

Changes

Cohort / File(s) Summary
Regression Test Export
playground/composables/async-value.ts
Adds new useSleep constant using useAsyncData with an async arrow function body to test async keyword handling in export parsing.
Reserved Words Filtering
src/node/scan-dirs.ts
Introduces JS_RESERVED_WORDS set containing JavaScript reserved words and adds conditional filtering logic in scanExports to skip reserved words when processing declaration exports, preventing false-positive captures.
Type Declaration Updates
test/dts.test.ts
Adds global type declaration for useSleep in the generated TypeScript declarations snapshot.
Export Scanning Tests
test/scan-dirs.test.ts
Extends test expectations to include useSleep export and adds new test verifying that reserved words like async are filtered out from scanned exports.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A sleepy function hops on by,
With async dreams beneath the sky,
Reserved words now know their place,
No false exports in the race!
Unimport hops with joy and care,
Filtering keywords everywhere! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: filter out reserved words from exp.names' directly describes the main change implemented across multiple files: adding JS_RESERVED_WORDS set and filtering logic in scan-dirs.ts to prevent reserved keywords from being captured as exports.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/reserved-keywords

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/node/scan-dirs.ts`:
- Around line 17-71: The JS_RESERVED_WORDS set is too broad and causes valid
identifier exports like "async" or "of" to be dropped; update the filtering
logic that uses JS_RESERVED_WORDS (and the related checks referenced at the
other use sites around the 201-202 area) so it only excludes tokens when they
originate from parser false-positives emitted by mlly (i.e., when metadata/flags
indicate a lexer/parser artifact), and do not apply the reserved-word filter to
explicitly declared exports or identifiers parsed as real declarations; locate
the usage of JS_RESERVED_WORDS in scan-dirs.ts and change the condition to first
detect explicit export/declaration nodes (or mlly false-positive markers) and
only then consult JS_RESERVED_WORDS.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2378af8f-228c-40ff-b2fc-5fecf3302b4b

📥 Commits

Reviewing files that changed from the base of the PR and between 47cffcf and dc242ee.

📒 Files selected for processing (4)
  • playground/composables/async-value.ts
  • src/node/scan-dirs.ts
  • test/dts.test.ts
  • test/scan-dirs.test.ts

Comment thread src/node/scan-dirs.ts
Comment on lines +17 to +71
const JS_RESERVED_WORDS = new Set([
// Keywords
'abstract',
'arguments',
'async',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'of',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'undefined',
'var',
'void',
'while',
'with',
'yield',
])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and the relevant code
find . -name "scan-dirs.ts" -type f

Repository: unjs/unimport

Length of output: 80


🏁 Script executed:

# Get the file and examine the context around lines 17-71 and 201-202
if [ -f "src/node/scan-dirs.ts" ]; then
  echo "=== Lines 1-100 of scan-dirs.ts ==="
  head -100 src/node/scan-dirs.ts | cat -n
fi

Repository: unjs/unimport

Length of output: 2974


🏁 Script executed:

# Also check lines around 201-202
if [ -f "src/node/scan-dirs.ts" ]; then
  echo "=== Lines 190-210 of scan-dirs.ts ==="
  sed -n '190,210p' src/node/scan-dirs.ts | cat -n | sed 's/^/190+/'
fi

Repository: unjs/unimport

Length of output: 1236


🌐 Web query:

ECMAScript specification contextual keywords async of declaration identifiers export const

💡 Result:

In the ECMAScript spec, async and of are contextual (conditional) keywords: they are “always allowed as identifiers, but also appear as keywords within certain syntactic productions” (i.e., the parser treats them as keywords only in specific grammar positions). The spec explicitly lists: as, async, from, get, meta, of, set, target. [1]

async as an identifier vs keyword

Because async is contextual, code like let async = 1; is valid in contexts where the grammar is expecting an identifier, while async function f(){} uses async in a position where the grammar gives it keyword meaning. The spec also notes an important disambiguation rule with LineTerminators: if async is followed by a line break before function / IdentifierName / (, Automatic Semicolon Insertion can kick in and async won’t be parsed as part of an async construct. [1]

of in for...of

Similarly, of can be an identifier in general, but in the specific grammar for a for...of statement, of is treated as the keyword introducing the iteration RHS (that’s why it’s in the contextual-keyword list). [1]

Why export const ... is valid (grammar path)

The module grammar allows exporting declarations directly:

  • ExportDeclaration : export Declaration [2]
  • Declaration : LexicalDeclaration [2]
    And const declarations are LexicalDeclarations, so export const x = 1; is syntactically allowed via export Declaration → LexicalDeclaration. [2] (MDN summarizes this as: after export you can use let, const, var, function, or class declarations.) [3]

Over-broad reserved-word filtering drops legitimate declaration exports.

The filter unconditionally skips all words in JS_RESERVED_WORDS, including contextual keywords like async and of. Per ECMAScript spec, these contextual keywords are valid identifiers and can be legitimately exported: export const async = 1 and export const of = 1 are valid. The current approach prevents these valid exports from being imported, creating false negatives.

Gate the filter to only remove parser false positives from mlly, not explicitly declared exports:

Proposed safer pattern
+  const hasExplicitExportDeclaration = (name: string) => {
+    const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+    return new RegExp(
+      String.raw`\bexport\s+(?:const|let|var|function|class|enum)\s+${escaped}\b`,
+      'm',
+    ).test(code)
+  }

   async function toImport(exports: ESMExport[], additional?: Partial<Import>) {
     for (const exp of exports) {
       // ...
       else if (exp.type === 'declaration') {
         for (const name of exp.names) {
-          if (JS_RESERVED_WORDS.has(name))
+          if (JS_RESERVED_WORDS.has(name) && !hasExplicitExportDeclaration(name))
             continue
           imports.push({ name, as: name, from: filepath, ...additional })

Also applies to: 201-202

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/node/scan-dirs.ts` around lines 17 - 71, The JS_RESERVED_WORDS set is too
broad and causes valid identifier exports like "async" or "of" to be dropped;
update the filtering logic that uses JS_RESERVED_WORDS (and the related checks
referenced at the other use sites around the 201-202 area) so it only excludes
tokens when they originate from parser false-positives emitted by mlly (i.e.,
when metadata/flags indicate a lexer/parser artifact), and do not apply the
reserved-word filter to explicitly declared exports or identifiers parsed as
real declarations; locate the usage of JS_RESERVED_WORDS in scan-dirs.ts and
change the condition to first detect explicit export/declaration nodes (or mlly
false-positive markers) and only then consult JS_RESERVED_WORDS.

@antfu
antfu merged commit 464a023 into main Mar 5, 2026
4 checks passed
@antfu
antfu deleted the fix/reserved-keywords branch March 5, 2026 08:31
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.

2 participants