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:164 — attribute(). 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:700 — namespace(), reading this.nextToken[TOKEN.TYPE] for a trailing |.
src/parser.js:808 — parentheses(), 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.
Selectors that run out of tokens before a bracket is closed throw
TypeError: Cannot read properties of undefinedrather than the parser's ownExpected 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:
Reproduced on a clean install of
postcss-selector-parser@7.1.4, and still present onmain(
4a7e4e3).Cause
Three places dereference a token that is
undefinedoncepositionhas passed the end oftokens:src/parser.js:164—attribute(). Thewhileloop exits on either a closing bracketor running out of tokens, and the check immediately after assumes the former.
src/parser.js:700—namespace(), readingthis.nextToken[TOKEN.TYPE]for a trailing|.src/parser.js:808—parentheses(), theunbalancedbranch.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:
postcss-modules-local-by-defaultis on thecss-loaderpath, so a stray|surfaces as aTypeErrorfrom inside the parser rather than aCssSyntaxError. stylelint degrades moregracefully — it reports
Cannot parse selector (TypeError: …)— but the underlying cause is thesame.
Why the existing tests don't catch it
src/__tests__/exceptions.mjs:8already covers this input shape:and it passes. The
throwshelper asserts{ instanceOf: Error }when no message is supplied(
src/__tests__/util/helpers.mjs:33), andTypeErrorsatisfies that — so the assertion can'tdistinguish 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: Erroragain. 781/781 tests pass,
oxlintandformat:checkclean, coverage thresholds met.Environment:
postcss-selector-parser@7.1.4, Node 24.16.0, macOS.