Skip to content

Commit 6f4e6c1

Browse files
theRizwanMoOx
andauthored
fix: TypeError on unclosed [, ( and trailing | (#330)
Selectors that ran out of tokens before a bracket closed threw `TypeError: Cannot read properties of undefined` instead of the parser's own `Expected a closing ...` error. The error path already existed in each case; it just could not be reached, because building the message dereferenced the token that was missing. parser().astSync('a]') // Expected an opening square bracket. (ok) parser().astSync('a[href') // TypeError: ...reading '0' Closing delimiters with no opener were already handled properly, so this brings the two directions into line. - attribute(): the while loop exits on either a closing bracket or end of input, and the check after it assumed the former. Errors now point at the opening bracket. - namespace(): a trailing `|` with nothing after it now reaches the existing unexpectedPipe(), which reports against currToken. - parentheses(): the unbalanced branch falls back to the opening token. No behaviour change for input that already parsed, and no existing error message changes. Tests assert the message rather than the type. exceptions.mjs already covered this input shape via `throws("unclosed attribute selector", ...)`, which passed throughout: `throws` falls back to `{instanceOf: Error}` when no message is given, and TypeError satisfies that. Co-authored-by: Max T. <git@moox.io>
1 parent 4d8437f commit 6f4e6c1

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

src/__tests__/exceptions.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ throws("unclosed pseudo element", "button::");
77
throws("unclosed pseudo class", "a:");
88
throws("unclosed attribute selector", '[name="james"][href');
99

10+
// Constructs left open at end of input. These threw a raw TypeError before
11+
// `attribute`, `namespace` and `parentheses` guarded against running out of
12+
// tokens. Asserted by message rather than by type: the default
13+
// `{instanceOf: Error}` check is satisfied by a TypeError, which is why the
14+
// existing "unclosed attribute selector" case above passed throughout.
15+
throws(
16+
"unclosed attribute at end of input",
17+
"a[href",
18+
"Expected a closing square bracket.",
19+
);
20+
throws(
21+
"unclosed attribute with value at end of input",
22+
"a[href=x",
23+
"Expected a closing square bracket.",
24+
);
25+
throws("trailing namespace pipe", ".foo|", "Unexpected '|'.");
26+
throws(
27+
"unclosed parenthesis at end of input",
28+
"a(",
29+
"Expected a closing parenthesis.",
30+
);
31+
1032
throws("no opening parenthesis", ")");
1133
throws("no opening parenthesis (2)", ":global.foo)");
1234
throws("no opening parenthesis (3)", "h1:not(h2:not(h3)))");

src/parser.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ export default class Parser {
161161
attr.push(this.currToken);
162162
this.position++;
163163
}
164+
if (!this.currToken) {
165+
// Ran off the end of the token stream: the attribute was never closed.
166+
// Point at the opening bracket, which is where the author needs to look.
167+
return this.expected("closing square bracket", startingToken[TOKEN.START_POS]);
168+
}
164169
if (this.currToken[TOKEN.TYPE] !== tokens.closeSquare) {
165170
return this.expected("closing square bracket", this.currToken[TOKEN.START_POS]);
166171
}
@@ -707,6 +712,11 @@ export default class Parser {
707712
prev[TOKEN.TYPE] === tokens.ampersand)
708713
? this.content(prev)
709714
: true;
715+
if (!this.nextToken) {
716+
// A trailing `|` with nothing after it. `unexpectedPipe` reports against
717+
// `currToken`, which is the pipe itself and always present here.
718+
return this.unexpectedPipe();
719+
}
710720
if (this.nextToken[TOKEN.TYPE] === tokens.word) {
711721
this.position++;
712722
return this.word(before);
@@ -740,6 +750,7 @@ export default class Parser {
740750
parentheses() {
741751
let last = this.current.last;
742752
let unbalanced = 1;
753+
const openingToken = this.currToken;
743754
this.position++;
744755
if (last && last.type === types.PSEUDO) {
745756
const selector = new Selector({
@@ -815,7 +826,12 @@ export default class Parser {
815826
}
816827
}
817828
if (unbalanced) {
818-
return this.expected("closing parenthesis", this.currToken[TOKEN.START_POS]);
829+
// `currToken` is undefined when the token stream ran out before the
830+
// parenthesis was closed; fall back to the opening one.
831+
return this.expected(
832+
"closing parenthesis",
833+
(this.currToken || openingToken)[TOKEN.START_POS],
834+
);
819835
}
820836
}
821837

0 commit comments

Comments
 (0)