Skip to content

linter: malformed glob in overrides.files is silently ignored instead of reported #24612

Description

@ThomasRopke

What version of Oxlint are you using?

1.74.0 (also reproduced on 1.11.1; behaviour differs from 1.11.0)

What command did you run?

oxlint -c .oxlintrc.json src/app.js

What does your .oxlintrc.json (or oxlint.config.ts) config file look like?

// The closing `}` of the brace expansion is missing — an ordinary typo.
{
  "rules": { "no-debugger": "error" },
  "overrides": [
    { "files": ["src/**/*.{js,ts"], "rules": { "no-debugger": "off" } }
  ]
}

With src/app.js containing just debugger;.

What happened?

A malformed glob in overrides.files is accepted without any diagnostic, and
whether it matches is undefined — it has changed between releases, silently
changing which rules apply.

1.11.0 rejects the config, naming the problem and suggesting the fix:

Failed to parse configuration file.
  x Failed to parse config with error Error("error parsing glob 'src/**/*.{js,ts':
    unclosed alternate group; missing '}' (maybe escape '{' with '[{]'?)", line: 0, column: 0)

1.11.1 and later accept it silently. But the effect is not stable:

overrides.files 1.11.0 1.11.1 1.74.0
(no override) rule fires rule fires rule fires
src/**/*.{js,ts} — valid, matches rule off rule off rule off
other/**/*.{js,ts} — valid, no match rule fires rule fires rule fires
src/**/*.{js,ts — malformed config error rule off (matched) rule fires (did not match)

The three control rows are identical across all versions; only the malformed
pattern diverges. So on 1.11.1 the typo silently enabled the override, and by
1.74.0 the same config silently disabled it — no error, no warning, at any
point. A user carrying this typo through an upgrade had the set of rules applied
to their files change underneath them.

The same holds for an unclosed character class (src/[abpp.js vs src/[ab]pp.js).

For contrast, oxlint is strict and helpful about everything else in the same
file — which is what makes the glob case read as "my override is broken" rather
than "my pattern is invalid":

$ oxlint -c bad.json src/     # {"rules": {"no-debugger": "erorr"}}
x Failed to parse config with error Error("Failed to parse rule severity, expected
  one of "allow", "off", "deny", "error" or "warn", but got "erorr"")

$ oxlint -c bad2.json src/    # {"overrides": [{"filez": [...]}]}
x Failed to parse config with error Error("unknown field `filez`, expected one of
  `files`, `excludeFiles`, `env`, `globals`, `plugins`, `jsPlugins`, `rules`")

Origin

oxlint_v1.11.0 is the last tag without
#12870 (238b183dfa); oxlint_v1.11.1
is the first with it. That matches the behaviour boundary above exactly.

That PR moved GlobSet from globset to fast-glob. Its review thread asked
the right question — whether matching could change — and got a careful answer
about *.txt being auto-expanded to **/*.txt and about */*.txt depth, both
of which the PR then compensated for in GlobSet::new (the **/ prefixing that
is still there today). Validation is a third category that the thread didn't
cover, and it changed too:

// before — invalid patterns surfaced through serde
Self::new(globs).map_err(de::Error::custom)

// after — cannot fail
Ok(Self::new(Vec::<String>::deserialize(deserializer)?))

globset::Glob::new returns Result, so a bad pattern errored at config load.
fast_glob::glob_match(glob: impl AsRef<[u8]>, path: impl AsRef<[u8]>) -> bool
is that crate's entire public surface — there's no parse or validate entry
point, so an invalid pattern has nowhere to be rejected and its result is
whatever the matcher happens to do with it. That also fits the PR carrying
C-cleanup ("not expected to change behavior") and CodSpeed reporting no
performance change: the loss looks incidental rather than chosen.

I'd guess the 1.11.1 → 1.74.0 difference is the workspace moving fast-glob
from 1.0.0 to 1.0.1 over that range, but I haven't bisected it — treat that
as unverified. The observable point stands either way: a patch-level dependency
bump can flip the meaning of a malformed pattern, because nothing defines what
it should mean.

I'm not suggesting reverting #12870 — it
deleted a real pile of boilerplate (the raw: Vec<String> kept beside
globs: globset::GlobSet for --print-config, plus four hand-written impls).
Restoring an error presumably means either a validation pass in GlobSet::new
or an upstream fast-glob API, and whether that's worth it is your call. If the
silent behaviour is known and accepted, please close.


Disclosure per the AI usage policy in CONTRIBUTING.md: this report was drafted
with AI assistance. I have reproduced and verified the behaviour myself before
filing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions