Skip to content

Unclosed [, ( or trailing | throws a raw TypeError instead of the parser's own error #329

Description

@theRizwan

Selectors that run out of tokens before a bracket is closed throw
TypeError: Cannot read properties of undefined rather than the parser's own
Expected a closing … error.

The asymmetry is the clearest way to see it — a closing delimiter with no opener is handled
properly, but an opening delimiter with no closer is not:

const parser = require('postcss-selector-parser');

parser().astSync('a]');      // Error: Expected an opening square bracket.   ✅
parser().astSync('a)');      // Error: Expected an opening parenthesis.      ✅

parser().astSync('a[href');  // TypeError: Cannot read properties of undefined (reading '0')
parser().astSync('a[href=x');// TypeError: Cannot read properties of undefined (reading '0')
parser().astSync('a(');      // TypeError: Cannot read properties of undefined (reading '5')
parser().astSync('.foo|');   // TypeError: Cannot read properties of undefined (reading '0')

Reproduced on a clean install of postcss-selector-parser@7.1.4, and still present on main
(4a7e4e3).

TypeError: Cannot read properties of undefined (reading '0')
    at Parser.attribute (.../dist/parser.js:203:27)
    at Parser.parse (.../dist/parser.js:969:22)
    at Parser.loop (.../dist/parser.js:947:18)

Cause

Three places dereference a token that is undefined once position has passed the end of
tokens:

  • src/parser.js:164attribute(). The while loop exits on either a closing bracket
    or running out of tokens, and the check immediately after assumes the former.
  • src/parser.js:700namespace(), reading this.nextToken[TOKEN.TYPE] for a trailing |.
  • src/parser.js:808parentheses(), the unbalanced branch.

Each already has the right error to raise — it just can't reach it, because building the
error message dereferences the missing token.

Why it matters beyond malformed input

postcss's own parser accepts some of these selectors, so they reach consumers rather than being
rejected upstream:

const postcss = require('postcss');
const localByDefault = require('postcss-modules-local-by-default');

postcss.parse('.foo| { color: red }');   // fine — valid CSS syntax to postcss
postcss([localByDefault()]).process('.foo| { color: red }', { from: 'app.css' });
// TypeError: Cannot read properties of undefined (reading '0')

postcss-modules-local-by-default is on the css-loader path, so a stray | surfaces as a
TypeError from inside the parser rather than a CssSyntaxError. stylelint degrades more
gracefully — it reports Cannot parse selector (TypeError: …) — but the underlying cause is the
same.

Why the existing tests don't catch it

src/__tests__/exceptions.mjs:8 already covers this input shape:

throws("unclosed attribute selector", '[name="james"][href');

and it passes. The throws helper asserts { instanceOf: Error } when no message is supplied
(src/__tests__/util/helpers.mjs:33), and TypeError satisfies that — so the assertion can't
distinguish a clean parser error from a crash.

Fix

Happy to open a PR — I have one ready: three EOF guards, plus regression tests asserting the
message rather than the type so this specific failure can't hide behind instanceOf: Error
again. 781/781 tests pass, oxlint and format:check clean, coverage thresholds met.

Environment: postcss-selector-parser@7.1.4, Node 24.16.0, macOS.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions