hudochenkov / postcss-styled-syntax

PostCSS syntax for CSS-in-JS like styled-components
MIT License
74 stars 4 forks source link

Parse styled components throws Expected a pseudo-class or pseudo-element #24

Closed michalpechnet closed 9 months ago

michalpechnet commented 10 months ago

I try to migrate to latest version of stylelint v15 in my project using styled-components. However I run into problem with parser.

Description

Stylelint parser throws Error: Expected a pseudo-class or pseudo-element when parsing styled-components code.

Error

Error: Expected a pseudo-class or pseudo-element.
    at /stylelint-styled-components-debug/src/Container.Styled.ts:71:3
    at Root._error (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/parser.js:130:16)
    at Root.error (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/selectors/root.js:30:19)
    at Parser.error (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/parser.js:596:21)
    at Parser.expected (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/parser.js:922:19)
    at Parser.pseudo (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/parser.js:728:19)
    at Parser.parse (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/parser.js:884:14)
    at Parser.loop (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/parser.js:853:12)
    at new Parser (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/parser.js:123:10)
    at Processor._root (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/processor.js:40:18)
    at Processor._runSync (/stylelint-styled-components-debug/node_modules/postcss-selector-parser/dist/processor.js:78:21)
error Command failed with exit code 1.

Example repository

I've prepared example repository where you can reproduce this error. See https://github.com/michalpechnet/stylelint-styled-components-debug.

Steps to reproduce

hudochenkov commented 10 months ago

Thank you for the reporting and providing the repository!

The error comes from postcss-selector-parser and not from postcss-styled-syntax. We see it in the stack trace. Stylelint uses postcss-selector-parser internally. Stylelint usually does a good job filtering non-standard selectors. But here it didn't catch incompatible syntax and we see an error.

There also issue with postcss-styled-syntax. Because it parser rule selector incorrectly.

Given this example:

export const StyledContainer = styled.div`
  ${(props) => props.prop1 && ''}

  ${(props) => props.prop2 && css`box-shadow: none;`}

  a {}
`

Parser produce a rule with selector ${(props) => props.prop2 &&\n css`box-shadow: none;`}\n\n a instead of a.

Need to find why this happens.

Issue is not reproducible if we don't have two interpolations one after another:

export const StyledContainer = styled.div`
  ${(props) => props.prop1 && ''}

  display: flex;

  ${(props) => props.prop2 && css`box-shadow: none;`}

  a {}
`
michalpechnet commented 10 months ago

Hi @hudochenkov , thanks for reply. I also created issue in postcss-selector-parser package. If there is any way, how can I help you or provide you more information, please let me know.

https://github.com/postcss/postcss-selector-parser/issues/285

hudochenkov commented 10 months ago

It's not a problem with postcss-selector-parser. postcss-styled-syntax produces wrong AST, then stylelint takes it and pass it postcss-selector-parser.

Most of the time we have checks in Stylelint to ignore non-standard CSS syntax. Most likely one of rules (or some of them) don't have such check. I would suggest open an issue in Stylelint and close issue in postcss-selector-parser.

When you open issue in Stylelint it would be nice if you could find which rules specifically are not ignoring non-standard CSS.

karlhorky commented 10 months ago

I recently received similar errors when upgrading to Prettier v3, which includes some different formatting heuristics for CSS-in-JS:

Original (unformatted) code

const styles = css`
  ${!isReviewingAnswer &&
  `&:hover {
      background-color: #ddd;
    }`}

  input {
    position: absolute;
    opacity: 0;
    cursor: pointer;
  }
`;

Problematic (formatted) code

const styles = css`
  ${!isReviewingAnswer &&
  css`
    &:hover {
      background-color: #ddd;
    }
  `} input {
    position: absolute;
    opacity: 0;
    cursor: pointer;
  }
`;

My error with the formatted code was:

Error: Unexpected '!'. Escaping special characters with \ may help.
    at /Users/k/p/courses/packages/quiz-multi-choice/index.tsx:216:3
    at Root._error (/Users/k/p/project/node_modules/postcss-selector-parser/dist/parser.js:130:16)
    at Root.error (/Users/k/p/project/node_modules/postcss-selector-parser/dist/selectors/root.js:30:19)
    at Parser.error (/Users/k/p/project/node_modules/postcss-selector-parser/dist/parser.js:596:21)
    at Parser.unexpected (/Users/k/p/project/node_modules/postcss-selector-parser/dist/parser.js:610:17)
    at Parser.parse (/Users/k/p/project/node_modules/postcss-selector-parser/dist/parser.js:908:14)
    at Parser.loop (/Users/k/p/project/node_modules/postcss-selector-parser/dist/parser.js:853:12)
    at new Parser (/Users/k/p/project/node_modules/postcss-selector-parser/dist/parser.js:123:10)
    at Processor._root (/Users/k/p/project/node_modules/postcss-selector-parser/dist/processor.js:40:18)
    at Processor._runSync (/Users/k/p/project/node_modules/postcss-selector-parser/dist/processor.js:78:21)
    at Processor.processSync (/Users/k/p/project/node_modules/postcss-selector-parser/dist/processor.js:164:23)
error Command failed with exit code 1.

The confusing part for me is that the ! from the error message was actually not the problem, but rather the lack of line break after the `}.

Adding a semicolon after the `} seems to resolve it for us:

Fixed (formatted) code

const styles = css`
  ${!isReviewingAnswer &&
  css`
    &:hover {
      background-color: #ddd;
    }
  `};

  input {
    position: absolute;
    opacity: 0;
    cursor: pointer;
  }
`;
karlhorky commented 10 months ago

Another failure case, this time not related to Prettier v3 formatting:

import { css } from '@emotion/react';

const breakpoint = '64em';

const chevronListItemStyles = css`
  @media (min-width: ${breakpoint}) {
    font-size: 10px;
  }
`;

Error:

pnpm stylelint '**/*.{js,tsx}'

components/ChevronListItem/index.tsx
 7:10  ✖  Unexpected invalid media query "(min-width: ${breakpoint})"  media-query-no-invalid

Workaround

import { css, SerializedStyles } from '@emotion/react';

function breakpoint(style: SerializedStyles) {
  return css`
    @media (min-width: 52em) {
      ${style}
    }
  `;
}

const chevronListItemStyles = css`
  ${breakpoint(css`
    font-size: ${theme.fontSizes[1]}px;
  `)}
`;
karlhorky commented 10 months ago

cc @43081j because it seems like you may be having similar errors in postcss-styled-components:

hudochenkov commented 10 months ago

Unexpected invalid media query "(min-width: ${breakpoint})" media-query-no-invalid

@karlhorky This should be reported to the Stylelint repository. media-query-no-invalid Stylelint rule should ignore non-standard media queries.

karlhorky commented 10 months ago

@hudochenkov hm, are you sure? It seems like media-query-no-invalid is specifically for the purpose of showing invalid CSS media queries?

media-query-no-invalid

Disallow invalid media queries.

Media queries must be grammatically valid according to the Media Queries Level 5 specification.

Or are you saying that this is grammatically valid?

I thought that postcss-styled-syntax would somehow interpolate this value so that ${ and } does not show up there... 🤔 But maybe I'm misunderstanding what postcss-styled-syntax is intended to do in this case.

hudochenkov commented 10 months ago

I'm sure. Core Stylelint rules are only targeted standard CSS syntax. @media (min-width: ${breakpoint}) {} is not a standard CSS syntax. For such cases many rules ignore non-standard syntaxes to avoid false positives.

I thought that postcss-styled-syntax would somehow interpolate this value so that ${ and } does not show up there... 🤔 But maybe I'm misunderstanding what postcss-styled-syntax is intended to do in this case.

postcss-styled-syntax does not compile or do any computations on the code. It's job is to take a string (source file) and transform it to PostCSS Abstract Syntax Tree (AST), so tool like Stylelint could work with it. For this example:

@media (min-width: ${breakpoint}) {
    font-size: 10px;
}

AST would say that there is a media at-rule with (min-width: ${breakpoint}) parameter. Then it's up to tool, which work with AST to decide what to do with that.

hudochenkov commented 9 months ago

Parsing issue should be fixed in 0.6.0.

karlhorky commented 9 months ago

Interesting, upgrading to postcss-styled-syntax@0.6.1 resolved some of the parsing issues, which is great 👍

However, the latest upgrade also broke this code pattern:

const StyledLink2 = styled('a', {
  shouldForwardProp,
})/* css */ `
  color: red;
`;

The css comment outside of the template string (which is used to enable CSS syntax highlighting even with the multi-line styled() function call) is causing a CssSyntaxError:

pnpm stylelint '**/*.{js,tsx}' 

components/Header/index.tsx
 49:1  ✖  Unknown word  CssSyntaxError

Removing the css comment (which disables the syntax highlighting) causes the error to disappear:

const StyledLink2 = styled('a', {
  shouldForwardProp,
})`
  color: red;
`;

Workaround 1

Use further interpolation to create the /* css */ comment:

const StyledLink2 = styled('a', {
  shouldForwardProp,
})`
  ${/* css */ `
    color: red;
  `}
`;

Workaround 2

Warning: If you use this method, verify that it is not altering your styling

Use an identity tagged template literal function (a no-op function) to enable the syntax highlighting:

// noop (identity) function to enable syntax highlighting for css
// https://github.com/hudochenkov/postcss-styled-syntax/issues/24#issuecomment-1872433249
const css = (
  strings: TemplateStringsArray,
  ...expressions: unknown[]
): string => {
  let result = strings[0]!;

  for (let i = 1, l = strings.length; i < l; i++) {
    result += expressions[i - 1];
    result += strings[i];
  }

  return result;
};

const StyledLink2 = styled('a', {
  shouldForwardProp,
})(css`
  color: red;
`);
karlhorky commented 9 months ago

I'm still receiving the "Unexpected invalid media query" error with postcss-styled-syntax@0.6.1 too...

But maybe that should be reported to Stylelint instead as @hudochenkov mentioned.

hudochenkov commented 9 months ago

However, the latest upgrade also broke this code pattern:

@karlhorky thank you for reporting! It is fixed in 0.6.2. Peculiar use case, never seen a comment between a tag function and template literal :)

But maybe that should be reported to Stylelint instead

Yes, it should.