Skip to content

fix: attribute selector with no valid attribute name - #335

Open
theRizwan wants to merge 1 commit into
postcss:mainfrom
theRizwan:fix/attribute-missing-name
Open

fix: attribute selector with no valid attribute name#335
theRizwan wants to merge 1 commit into
postcss:mainfrom
theRizwan:fix/attribute-missing-name

Conversation

@theRizwan

Copy link
Copy Markdown
Contributor

Fixes #334.

The crash

attribute() read next[TOKEN.TYPE] in four places without checking that a next token existed:

  • case tokens.asterisk
  • case tokens.caret (also reached by tokens.dollar via fall-through)
  • case tokens.combinator, the ~ check
  • case tokens.combinator, the | check

So an attribute selector whose last token before ] was *, $, ^, ~ or | threw a raw TypeError out of the parser rather than the parser's own error. [ns|*], [a$], [a|] and eight other shapes were affected.

Two other reads of next[TOKEN.TYPE] in the same file already guard with next &&, and the else if directly below the asterisk case guards with && next, so this follows the pattern already in the file rather than introducing one.

Why the guards alone were not enough

Adding the four guards moved the failure instead of fixing it. With no attribute name captured, Attribute#toString interpolates the missing value, so [ns|*] became the string [ns|undefined].

That path was already reachable without any crash. On 7.1.5:

parser().astSync("[ * ]").toString();   // "[ *|undefined]"

[*] on its own already threw Expected an attribute., so the two were inconsistent. The token loop now also rejects a bracket pair that supplied no attribute name, pointing at the opening bracket.

Verification

Compared against pristine 7.1.5 across 43,200 generated attribute selectors, varying the name, namespace separator and its surrounding whitespace, operator, value, quoting, insensitivity flag, and padding:

7.1.5 this branch
raw TypeError 341 0
output containing the text undefined 596 0
output differs where both parsed 0

The 596 that now raise Expected an attribute. are exactly the 596 that previously emitted undefined. No input that previously parsed produces different output.

npm test is green, including oxlint, the type check, and the coverage thresholds: 94.98% lines, 95.54% branches, 97.71% functions against gates of 94/94/96. Test count 789 to 801.

Test placement

The newly-erroring cases are in exceptions.mjs, asserted by message rather than by type, for the reason recorded in that file in #330: the default {instanceOf: Error} check is satisfied by a TypeError, so a type-only assertion would not have caught this.

The five inputs that now parse instead of crashing ([href*], [href$], [href^], [href~], [href|], each yielding [href]) are in attributes.mjs using ava directly rather than the test helper. test adds an automatic round-trip assertion, and these are deliberately not round-trips: the trailing token is dropped, matching how [href=] already drops an operator with no value. Using test would have asserted the lossy output is correct.

Scope

[ns | ] still returns [ns ] rather than treating ns as a namespace. That is the whitespace-before-| lookahead in #229 and is left alone here.

Preserving a trailing */$/^/~/| so those five round-trip is also out of scope. It is the same lossy behaviour as [href=] and would be a separate change.

Disclosure

I used AI assistance while investigating and writing this, including for the differential fuzz harness. I have read and can account for every line, and the reasoning above is mine. Flagging it because of the preference stated on #333; happy to walk through any part of it.

`attribute()` read `next[TOKEN.TYPE]` in four places without checking that a
next token existed, so an attribute selector whose last token before `]` was
`*`, `$`, `^`, `~` or `|` threw a raw TypeError out of the parser. Two other
reads of `next` in the same file already guarded with `next &&`.

Guarding those four alone moved the failure rather than fixing it: with no
attribute name captured, `Attribute#toString` interpolated the missing value
and emitted a selector containing the literal text `undefined`. That path was
already reachable without any crash, for example `[ * ]` produced
`[ *|undefined]`. So the token loop now also rejects a bracket pair that
supplied no attribute name, which matches the error `[*]` already produced.

Across 43,200 generated attribute selectors compared against 7.1.5: 341 raw
TypeErrors and 596 outputs containing `undefined` are both eliminated, and no
input that previously parsed produces different output.

Refs postcss#334

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Attribute selector with no valid attribute name throws a raw TypeError, or emits the text "undefined"

1 participant