Skip to content

excludeRE[3] with /s flag causes cross-line backtracking that falsely excludes auto-imports #525

Description

@felixgabler

What happens

The variable declaration regex in excludeRE:

/\b(?:const|let|var)\s+?(\[.*?\]|\{.*?\}|.+?)\s*?[=;\n]/gs

can backtrack across hundreds of lines when a for (const [...] of ...) destructuring appears. The /s flag makes . match newlines, so when the \[.*?\] alternative fails to match (because ] is followed by of, not =/;/\n), the engine expands .*? across the rest of the file until it finds another ] followed by =.

All identifiers in that span end up in group 1, get split by separatorRE, and are falsely marked as "locally declared" — removing them from auto-import detection.

Reproduction

Given a file like:

// utils/native.ts (auto-imported by Nuxt)
export const PRODUCTION_ORIGIN = 'https://example.com';

And a consumer file:

// some-file.ts
async function doWork() {
    const results = await Promise.allSettled([task1(), task2()]);
    for (const [i, r] of results.entries()) {
        // ^ excludeRE starts matching here: `const [`
        // The `]` is followed by ` of`, not `= ; \n`, so backtracking begins
        if (r.status === 'rejected') logError(r.reason);
    }
}

// ... many lines ...

function patchFetch() {
    const targetUrl = `${PRODUCTION_ORIGIN}/api`;
    //                    ^ PRODUCTION_ORIGIN is now inside the excludeRE match span
    //                      and gets falsely excluded from auto-import detection

    headers[key] = value;
    // ^ backtracking finally stops here: `]` followed by ` =`
}

PRODUCTION_ORIGIN is not auto-imported, causing a runtime ReferenceError: Can't find variable: PRODUCTION_ORIGIN.

Impact

We've hit this in production in a Nuxt + Capacitor app. The silent failure (no build error, just a missing import at runtime) makes it hard to catch. We've worked around it with explicit import { x } from '#imports' in affected files.

Analysis

The eslint-disable-next-line regexp/no-super-linear-backtracking comment on the regex suggests the backtracking concern was known but suppressed. The /s flag is what makes this actively harmful rather than just slow — without it, .*? can't cross line boundaries and the match stays local.

Possible fix

A few approaches come to mind (happy to contribute a PR if one of these looks right):

  1. Remove the /s flag — the \n in the terminator set [=;\n] already handles multi-line const declarations, and without /s the backtracking stays within one line
  2. Make the \[...\] alternative more specific: \[.*?\]\s*?= to only match array destructuring assignments, not for-of
  3. Use a negative lookahead after ] to reject of keyword: \[.*?\](?!\s+of\b)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions