├── .npmignore
├── .eslintignore
├── .prettierignore
├── .prettierrc.yaml
├── .gitignore
├── test
├── jest-setup.js
├── fixtures
│ ├── options
│ │ ├── module-name.js
│ │ ├── relative-module-name.js
│ │ ├── import-name.js
│ │ ├── strict.js
│ │ ├── invalid-module-name.js
│ │ └── invalid-import-name.js
│ ├── garbage-css
│ │ └── invalid-css.js
│ ├── inject-global
│ │ ├── valid-spaces.js
│ │ └── valid-tabs.js
│ ├── simple
│ │ ├── other-library.js
│ │ ├── invalid.js
│ │ ├── global.js
│ │ ├── parse.js
│ │ ├── imports.js
│ │ ├── valid.js
│ │ ├── identify-styled.js
│ │ ├── nesting.js
│ │ └── helpers.js
│ ├── interpolation-tagging
│ │ ├── invalid-tag.js
│ │ ├── invalid-custom.js
│ │ └── valid.js
│ ├── ignore-rule-comments
│ │ ├── disable-whole-file.js
│ │ ├── mix-in-css-disables.js
│ │ ├── use-single-line-comments.js
│ │ ├── alternating-disable-enable.js
│ │ └── use-single-line-disables.js
│ ├── hard
│ │ ├── valid-js-comments.js
│ │ ├── indentation.js
│ │ ├── source-maps.js
│ │ └── invalid-indentation.js
│ ├── typescript
│ │ ├── ts-syntax-no-styled-components.tsx
│ │ ├── ts-syntax-valid.ts
│ │ ├── ts-syntax-parse-error.ts
│ │ ├── ts-syntax-jsx-invalid.tsx
│ │ └── ts-syntax-invalid.ts
│ ├── interpolations
│ │ ├── invalid.js
│ │ ├── complex.js
│ │ └── valid.js
│ └── real-world
│ │ ├── LineNumbersReportedAccurate.js
│ │ └── Circle.js
├── emptyblock.test.js
├── emptycode.test.js
├── garbage-css.test.js
├── real-world.test.js
├── nofiles.test.js
├── interpolation-tagging.test.js
├── inject-global.test.js
├── interpolations.test.js
├── typescript.test.js
├── hard.test.js
├── ignore-rule-comments.test.js
├── simple.test.js
├── options.test.js
└── utils.test.js
├── .editorconfig
├── src
├── parsers
│ ├── babylon-parser.js
│ └── index.js
├── utils
│ ├── result.js
│ ├── parse.js
│ ├── styled.js
│ ├── general.js
│ └── tagged-template-literal.js
└── index.js
├── .eslintrc.js
├── .travis.yml
├── LICENSE.md
├── .github
└── ISSUE_TEMPLATE.md
├── package.json
├── CHANGELOG.md
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | .travis.yml
2 | .eslintrc
3 | .eslintignore
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | test/fixtures/**/*.js
2 | lib/
3 | coverage/
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | test/fixtures/**/*.js
2 | lib/
3 | coverage/
4 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | printWidth: 100
2 | tabWidth: 2
3 | semi: false
4 | singleQuote: true
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode
3 | *.log
4 | npm-debug.log*
5 | .DS_Store
6 | lib
7 | coverage/
8 |
--------------------------------------------------------------------------------
/test/jest-setup.js:
--------------------------------------------------------------------------------
1 | const ASYNC_TEST_TIMEOUT = process.env.CI ? 20 * 1000 : 5 * 1000
2 | jest.setTimeout(ASYNC_TEST_TIMEOUT)
3 |
--------------------------------------------------------------------------------
/test/fixtures/options/module-name.js:
--------------------------------------------------------------------------------
1 | import emotion from 'emotion'
2 |
3 | // ⚠️ EMPTY BLOCK ⚠️
4 | const Button = emotion.div`
5 |
6 | `
7 |
--------------------------------------------------------------------------------
/test/fixtures/garbage-css/invalid-css.js:
--------------------------------------------------------------------------------
1 | import styled from 'stylelint';
2 |
3 | const Button = styled.div`
4 | This is not actually css
5 | `;
6 |
--------------------------------------------------------------------------------
/test/fixtures/options/relative-module-name.js:
--------------------------------------------------------------------------------
1 | import emotion from '../../emotion'
2 |
3 | // ⚠️ EMPTY BLOCK ⚠️
4 | const Button = emotion.div`
5 |
6 | `
7 |
--------------------------------------------------------------------------------
/test/fixtures/options/import-name.js:
--------------------------------------------------------------------------------
1 | import { notDefault } from 'styled-components'
2 |
3 | // ⚠️ EMPTY BLOCK ⚠️
4 | const Button = notDefault.div`
5 |
6 | `
7 |
--------------------------------------------------------------------------------
/test/fixtures/inject-global/valid-spaces.js:
--------------------------------------------------------------------------------
1 | import { injectGlobal } from 'styled-components';
2 |
3 | injectGlobal`
4 | html {
5 | color: blue;
6 | }
7 | `;
8 |
--------------------------------------------------------------------------------
/test/fixtures/simple/other-library.js:
--------------------------------------------------------------------------------
1 | import styled from 'some-other-lib'
2 |
3 | // ⚠️ BAD INDENTATION ⚠️
4 | const Button2 = styled.button`
5 | color: blue;
6 | `
7 |
--------------------------------------------------------------------------------
/test/fixtures/inject-global/valid-tabs.js:
--------------------------------------------------------------------------------
1 | import { injectGlobal } from 'styled-components';
2 |
3 | // Use tabs
4 | injectGlobal`
5 | html {
6 | color: blue;
7 | }
8 | `;
9 |
--------------------------------------------------------------------------------
/test/fixtures/options/strict.js:
--------------------------------------------------------------------------------
1 | import { foo, bar } from 'some-module'
2 |
3 | // EMPTY BLOCK
4 | const Button = foo`
5 |
6 | `;
7 | const Button2 = bar`
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/test/fixtures/interpolation-tagging/invalid-tag.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Button1 = styled.button`
4 | color: red;
5 | ${/* sc-invalid */ 'dummy'}
6 | `;
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 2
6 | end_of_line = lf
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/test/fixtures/interpolation-tagging/invalid-custom.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Button1 = styled.button`
4 | color: red;
5 | ${/* sc-custom no quotes present */ 'dummy'}
6 | `;
7 |
--------------------------------------------------------------------------------
/test/fixtures/options/invalid-module-name.js:
--------------------------------------------------------------------------------
1 | import something from 'some-lib'
2 |
3 |
4 | // Empty block, but moduleName isn't set to some-lib
5 | // so shouldn't be an error
6 | const Button = something.div`
7 |
8 | `
9 |
--------------------------------------------------------------------------------
/test/fixtures/options/invalid-import-name.js:
--------------------------------------------------------------------------------
1 | import { something } from 'styled-components'
2 |
3 |
4 | // Empty block, but importName isn't set to `something`
5 | // so shouldn't be an error
6 | const Button = something.div`
7 |
8 | `
9 |
--------------------------------------------------------------------------------
/test/fixtures/simple/invalid.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | // ⚠️ EMPTY BLOCK ⚠️
4 | const Button = styled.button`
5 |
6 | `
7 |
8 | // ⚠️ BAD INDENTATION ⚠️
9 | const Button2 = styled.button`
10 | color: blue;
11 | `
12 |
--------------------------------------------------------------------------------
/test/fixtures/ignore-rule-comments/disable-whole-file.js:
--------------------------------------------------------------------------------
1 | /* stylelint-disable */
2 | import styled from 'styled-components'
3 |
4 | const Button1 = styled.button`
5 | color: red;
6 | `;
7 |
8 | const Button2 = styled.button`
9 | color: blue;
10 | `;
11 |
--------------------------------------------------------------------------------
/test/fixtures/hard/valid-js-comments.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Button1 = styled.div`
4 | color: red;
5 | display: block;
6 | // Some comment
7 | margin: 20px 30px 40px 50px; // comment after line
8 | padding: 20px;
9 | `;
10 |
11 |
--------------------------------------------------------------------------------
/test/fixtures/simple/global.js:
--------------------------------------------------------------------------------
1 | // Global variables
2 |
3 | // ⚠️ Wrong indentation ⚠️
4 | const Button = styled.div`
5 | color: red;
6 | `
7 |
8 | // ⚠️ Wrong indentation ⚠️
9 | const animation = keyframes`
10 | 0% {
11 | opacity: 0;
12 | }
13 | `
14 |
15 | const styles = css`
16 | color: blue;
17 | `
18 |
--------------------------------------------------------------------------------
/test/fixtures/typescript/ts-syntax-no-styled-components.tsx:
--------------------------------------------------------------------------------
1 | export interface IAccordionContainerProps {
2 | className?: string;
3 | borderWidth: number,
4 | onChange?(id: string): void;
5 | }
6 | export interface IAccordionContainerState {
7 | selected: string;
8 | }
9 |
10 | export default (prop:IAccordionContainerProps) => (
Test
);
--------------------------------------------------------------------------------
/test/fixtures/ignore-rule-comments/mix-in-css-disables.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Button1 = styled.button`
4 | color: red; /* stylelint-disable-line */
5 | `;
6 |
7 | /* stylelint-disable */
8 | const Button2 = styled.button`
9 | color: red;
10 | `;
11 |
12 | /* stylelint-enable */
13 | const Button3 = styled.button`
14 | /* stylelint-disable-next-line */
15 | color: red;
16 | `;
17 |
--------------------------------------------------------------------------------
/test/fixtures/interpolations/invalid.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const color = 'red'
4 |
5 | // ⚠️ BAD INDENTATION ⚠️
6 | const Button2 = styled.button.attrs({
7 | type: 'normal'
8 | })`
9 | display: block;
10 | ${
11 | props => props.isHovering && interpolatedStyle
12 | }
13 | position: ${
14 | props => props.position
15 | };
16 | color: ${color};
17 | background: blue;
18 | `
19 |
--------------------------------------------------------------------------------
/test/fixtures/simple/parse.js:
--------------------------------------------------------------------------------
1 | // asyncGenerators
2 | async function* test(x) {
3 | // objectRestSpread
4 | const a = {}
5 | x = { x, ...a }
6 |
7 | // dynamicImport
8 | const b = import('./somewhere')
9 |
10 | // optionalCatchBinding
11 | try {
12 | throw 0
13 | } catch {
14 | x = 1
15 | }
16 |
17 | // optionalChaining
18 | const op = x?.a
19 |
20 | // nullishCoalescingOperator
21 | x = x ?? 42
22 | }
23 |
--------------------------------------------------------------------------------
/test/fixtures/ignore-rule-comments/use-single-line-comments.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Button1 = styled.button`
4 | color: red;
5 | `;
6 |
7 | // stylelint-disable
8 | const Button2 = styled.button`
9 | color: red;
10 | `;
11 |
12 | // stylelint-enable
13 | const Button3 = styled.button`
14 | color: red;
15 | `;
16 |
17 | // stylelint-disable
18 | const Button4 = styled.button`
19 | color: red;
20 | `;
21 |
--------------------------------------------------------------------------------
/test/fixtures/ignore-rule-comments/alternating-disable-enable.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Button1 = styled.button`
4 | color: red;
5 | `;
6 |
7 | /* stylelint-disable */
8 | const Button2 = styled.button`
9 | color: red;
10 | `;
11 |
12 | /* stylelint-enable */
13 | const Button3 = styled.button`
14 | color: red;
15 | `;
16 |
17 | /* stylelint-disable */
18 | const Button4 = styled.button`
19 | color: red;
20 | `;
21 |
--------------------------------------------------------------------------------
/test/fixtures/ignore-rule-comments/use-single-line-disables.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | // stylelint-disable-next-line
4 | const Button1 = styled.button`
5 | color: red;
6 | `;
7 |
8 | /* stylelint-disable */
9 | const Button2 = styled.button`
10 | color: red;
11 | `;
12 |
13 | /* stylelint-enable */
14 | // stylelint-disable-next-line
15 | /* stylelint-disable-line */
16 | const Button3 = styled.button`
17 | color: red;
18 | `;
19 |
--------------------------------------------------------------------------------
/src/parsers/babylon-parser.js:
--------------------------------------------------------------------------------
1 | const babylon = require('@babel/parser')
2 |
3 | module.exports = (type, plugins) => input =>
4 | babylon.parse(input, {
5 | sourceType: 'module',
6 | plugins: [type].concat(
7 | plugins || [
8 | 'jsx',
9 | ['decorators', { decoratorsBeforeExport: true }],
10 | 'classProperties',
11 | 'exportExtensions',
12 | 'functionBind',
13 | 'functionSent'
14 | ]
15 | )
16 | })
17 |
--------------------------------------------------------------------------------
/test/fixtures/simple/imports.js:
--------------------------------------------------------------------------------
1 | import notStyled, {
2 | css as notCss,
3 | keyframes as notKeyframes,
4 | injectGlobal as notInjectGlobal
5 | } from 'styled-components'
6 |
7 | // ⚠️ BAD INDENTATION ⚠️
8 | const Button2 = notStyled.button`
9 | color: blue;
10 | `
11 |
12 | const styles = notCss`
13 | color: blue;
14 | `
15 |
16 | const animation = notKeyframes`
17 | 0% {
18 | opacity: 0;
19 | }
20 |
21 | 100% {
22 | opacity: 1;
23 | }
24 | `
25 |
26 | notInjectGlobal`
27 | html {
28 | color: blue;
29 | }
30 | `
31 |
--------------------------------------------------------------------------------
/test/fixtures/simple/valid.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | // Normal styled component
4 | const Button = styled.button`
5 | color: blue;
6 | `
7 |
8 | const Box = styled.div`
9 | color: blue;
10 | `
11 |
12 | // Tagname notation
13 | const Button2 = styled('button')`
14 | color: red;
15 | `
16 |
17 | const Box2 = styled('div')`
18 | color: red;
19 | `
20 |
21 | // Component Notation
22 | const Button3 = styled(Button2)`
23 | color: violet;
24 | `
25 |
26 | const Box3 = styled(Box2)`
27 | color: violet;
28 | `
29 |
--------------------------------------------------------------------------------
/test/fixtures/real-world/LineNumbersReportedAccurate.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Content = styled.section`
4 |
5 | ${props => props.centered &&
6 | `
7 | text-align: center;
8 | `}
9 |
10 | a {
11 | color: ${({ theme }) => theme.colors.lightBlue};
12 | font-weight: bold;
13 | transition: 0.3s color ease-in;
14 | text-decoration: none;
15 | }
16 |
17 | strong {
18 | font-weight: 600;
19 | }
20 | ul,
21 | ol {
22 | margin: 0;
23 | }
24 | `;
25 |
26 | export default Content;
27 |
--------------------------------------------------------------------------------
/test/fixtures/typescript/ts-syntax-valid.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export interface IAccordionContainerProps {
5 | className?: string;
6 | borderWidth: number,
7 | onChange?(id: string): void;
8 | }
9 | export interface IAccordionContainerState {
10 | selected: string;
11 | }
12 |
13 | export const AccordionContainerDiv = styled.div`
14 | border: solid ${(props: IAccordionContainerProps) => props.borderWidth}px;
15 | border-bottom-width: 0;
16 | width: auto;
17 | `;
--------------------------------------------------------------------------------
/test/fixtures/typescript/ts-syntax-parse-error.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export interface IAccordionContainerProps {
5 | className?: string;
6 | borderWidth: number,
7 | onChange?(id: string): void;
8 | }
9 | export interface IAccordionContainerState {
10 | selected: string;
11 | }
12 |
13 | export const AccordionContainerDiv = styled.div<{ width#: string }>`
14 | border: solid ${(props: IAccordionContainerProps) => props.borderWidth}px;
15 | border-bottom-width: 0;
16 | width: auto;
17 | `;
--------------------------------------------------------------------------------
/test/fixtures/simple/identify-styled.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Button1 = styled.div`
4 | bad-selector {
5 | color: red;
6 | }
7 | `;
8 |
9 | const Button2 = styled(Button1)`
10 | bad-selector {
11 | color: red;
12 | }
13 | `;
14 |
15 | const Image1 = styled.img.attrs({ src: 'url' })`
16 | bad-selector {
17 | color: red;
18 | }
19 | `;
20 |
21 | const Image2 = styled(Image1).attrs({ src: 'newUrl' })`
22 | bad-selector {
23 | color: red;
24 | }
25 | `;
26 |
27 | const Image3 = Image2.extend`
28 | bad-selector2 {
29 | color: blue;
30 | }
31 | `;
32 |
--------------------------------------------------------------------------------
/test/fixtures/typescript/ts-syntax-jsx-invalid.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from 'styled-components';
3 |
4 | interface Props {
5 | header: React.ReactNode;
6 | body: React.ReactNode;
7 | }
8 | type SelectProps = { items: T[] }
9 | class Select extends React.Component, any> { }
10 |
11 | class MyComponent extends React.Component {
12 | render() {
13 | return
14 | {this.props.header}
15 | {this.props.body}
16 |
;
17 | }
18 | }
19 |
20 | const DecoratedComp = styled(MyComponent)`
21 | border: 2px;
22 | `;
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['airbnb-base', 'prettier'],
3 | plugins: ['prettier'],
4 | env: {
5 | jest: true,
6 | node: true
7 | },
8 | rules: {
9 | 'prettier/prettier': 1,
10 |
11 | // Repo preferences
12 | 'class-methods-use-this': 0,
13 | 'symbol-description': 0,
14 | 'no-unused-vars': [2, { varsIgnorePattern: '^_+$' }],
15 | 'import/no-extraneous-dependencies': 0,
16 | 'no-confusing-arrow': 0,
17 | 'no-else-return': 0,
18 | 'no-prototype-builtins': 0
19 | },
20 | parserOptions: {
21 | ecmaVersion: 2018,
22 | sourceType: 'module',
23 | ecmaFeatures: {
24 | jsx: true
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | - "10"
5 | before_install:
6 | - npm i -g greenkeeper-lockfile@1
7 | - npm i -g coveralls
8 | install:
9 | - npm install
10 | before_script:
11 | - greenkeeper-lockfile-update
12 | script:
13 | - npm test
14 | after_script:
15 | - greenkeeper-lockfile-upload
16 | notifications:
17 | email:
18 | on_failure: change
19 | jobs:
20 | include:
21 | - stage: lint
22 | node_js: "8"
23 | script:
24 | - npm run lint:eslint
25 | - npm run lint:prettier
26 | - stage: coverage
27 | node_js: "8"
28 | script:
29 | - npm run test -- --coverage && cat ./coverage/lcov.info | coveralls
30 |
--------------------------------------------------------------------------------
/test/fixtures/simple/nesting.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | // Nesting
4 | const Button = styled.button`
5 | > h1 {
6 | color: blue;
7 | }
8 | `
9 |
10 | // Selectors
11 | const Button2 = styled.button`
12 | color: red;
13 |
14 | &:hover {
15 | color: blue;
16 | }
17 | `
18 |
19 | // Complex selectors
20 | const Button3 = styled.button`
21 | color: red;
22 |
23 | &:placeholder {
24 | color: blue;
25 | }
26 |
27 | &::-webkit-input-placeholder {
28 | color: blue;
29 | }
30 | `
31 |
32 | const color = 'red'
33 |
34 | // Selectors + interpolations
35 | const Button4 = styled.button`
36 | color: ${color};
37 | background: ${color};
38 |
39 | &:hover {
40 | background-color: ${color};
41 | }
42 | `
43 |
--------------------------------------------------------------------------------
/test/fixtures/interpolations/complex.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 |
3 | const interpolatedStyle = css`
4 | background-color: gray;
5 | color: gray;
6 | `
7 |
8 | // Interpolation of chunk
9 | const Div = styled.div`
10 | ${interpolatedStyle}
11 | `
12 |
13 | // Conditional interpolation of chunk
14 | const Button = styled.button`
15 | ${props => props.isHovering && interpolatedStyle}
16 | `
17 |
18 | // #110
19 | const Bla = styled.div`
20 | ${Button} {
21 | ${something}: blue;
22 | }
23 |
24 | background: ${bla};
25 |
26 | ${someValue}
27 | `
28 |
29 | // Multi-line interpolations
30 | const MultiLineDiv = styled.div`
31 | color: ${
32 |
33 |
34 | 'red'
35 | };
36 |
37 | ${
38 |
39 |
40 | 'long values'
41 | }
42 | `;
43 |
--------------------------------------------------------------------------------
/test/fixtures/simple/helpers.js:
--------------------------------------------------------------------------------
1 | import styled, { css, keyframes, injectGlobal } from 'styled-components'
2 |
3 | /**
4 | * Valid
5 | */
6 | const styles = css`
7 | color: blue;
8 | `
9 |
10 | const animation = keyframes`
11 | 0% {
12 | opacity: 1;
13 | }
14 | 100% {
15 | opacity: 0;
16 | }
17 | `
18 |
19 | const Button = styled.button`
20 | ${styles}
21 | animation: 3s ${animation};
22 | `
23 |
24 | injectGlobal`
25 | html {
26 | margin: 0;
27 | padding: 0;
28 | }
29 | `
30 |
31 | // ⚠ Indentation
32 | const styles2 = css`
33 | color: blue;
34 | `
35 |
36 | // ⚠ Indentation
37 | const animation2 = keyframes`
38 | 0% {
39 | opacity: 1;
40 | }
41 | 100% {
42 | opacity: 0;
43 | }
44 | `
45 |
46 | // ⚠ Indentation
47 | injectGlobal`
48 | html {
49 | margin: 0;
50 | padding: 0;
51 | }
52 | `
53 |
--------------------------------------------------------------------------------
/test/fixtures/hard/indentation.js:
--------------------------------------------------------------------------------
1 | import styled, { keyframes, css } from 'styled-components'
2 |
3 | // None of the below should throw indentation errors
4 | const Comp = () => {
5 | const Button = styled.button`
6 | color: blue;
7 | `
8 |
9 | return Button
10 | }
11 |
12 | const Comp2 = () => {
13 | const InnerComp = () => {
14 | const Button = styled.button`
15 | color: blue;
16 | `
17 |
18 | return Button
19 | }
20 |
21 | return InnerComp()
22 | }
23 |
24 | const Button = styled.button`color: blue;`
25 |
26 | const animations = {
27 | spinnerCircle: keyframes`
28 | 0% {
29 | opacity: 0;
30 | }
31 |
32 | 100% {
33 | opacity: 1;
34 | }
35 | `
36 | }
37 |
38 | const helper = condition => {
39 | if (condition) {
40 | return css`
41 | color: red;
42 |
43 | &:hover {
44 | color: blue;
45 | }
46 | `
47 | }
48 | return null
49 | }
50 |
--------------------------------------------------------------------------------
/test/fixtures/hard/source-maps.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | // ⚠️ BAD INDENTATION at 5:1 ⚠️
4 | const Button = styled.button`
5 | color: blue;
6 | `
7 |
8 | // Correct example
9 | const Button2 = styled.button`
10 | color: blue;
11 | `
12 |
13 | // ⚠️ BAD INDENTATION at 10:5 ⚠️
14 | const Button3 = styled.button`
15 | color: blue;
16 | `
17 |
18 | // ⚠️ BAD INDENTATION at 22:5 ⚠️
19 | const Button4 = styled.button`
20 | color: blue;
21 | background: ${color};
22 | display: block;
23 | `
24 |
25 | // ⚠️ BAD INDENTATION at 28:5 ⚠️
26 | const Button5 = styled.button`
27 | color: blue;
28 | background: ${color};
29 | display: block;
30 | `
31 |
32 | // ⚠️ BAD INDENTATION at 35:5 ⚠️
33 | const Button6 = styled.button`
34 | color: blue;
35 | ${`
36 | background: red;
37 | `}
38 | display: block;
39 | `
40 |
41 | // ⚠️ UNKNOWN PROPERTY at 42:31 ⚠️
42 | const Button7 = styled.button`unknown: blue;`
43 |
--------------------------------------------------------------------------------
/test/fixtures/interpolation-tagging/valid.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | // Test block
4 | const Button1 = styled.button`
5 | color: red;
6 | ${/* sc-block */ 'dummy'}
7 | `;
8 |
9 | // Test selector
10 | const Button2 = styled.button`
11 | color: red;
12 | ${/* sc-selector */ ':hover'} {
13 | background-color: blue;
14 | }
15 | ${/* sc-selector */ ':active'} {
16 | background-color: green;
17 | }
18 | `;
19 |
20 | // Test declaration
21 | const Button3 = styled.button`
22 | color: red;
23 | ${/* sc-declaration */ 'dummy'}
24 | ${/* sc-declaration */ 'dummy2'}
25 | `;
26 |
27 | // Test property
28 | const Button4 = styled.button`
29 | color: red;
30 | ${/* sc-property */ 'background-color'}: blue;
31 | `;
32 |
33 | // Test value
34 | const Button5 = styled.button`
35 | color: red;
36 | background-color: ${/* sc-value */ 'blue'};
37 | `;
38 |
39 | // Test custom
40 | const bool = true;
41 | const Button6 = styled.button`
42 | color: red;
43 | margin-${/* sc-custom 'left' */ bool ? 'left' : 'right'}: 10px;
44 | `;
45 |
--------------------------------------------------------------------------------
/test/fixtures/typescript/ts-syntax-invalid.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export interface IAccordionContainerProps {
5 | className?: string;
6 | borderWidth: number,
7 | onChange?(id: string): void;
8 | }
9 | export interface IAccordionContainerState {
10 | selected: string;
11 | }
12 |
13 | export const AccordionContainerDiv = styled.div`
14 | border: solid ${(props: IAccordionContainerProps) => props.borderWidth}px;
15 | border-bottom-width: 0;
16 | width: auto;
17 | `;
18 |
19 | // ⚠️ EMPTY BLOCK ⚠️
20 | export const Button = styled.button`
21 |
22 | `
23 |
24 | // ⚠️ BAD INDENTATION ⚠️
25 | export const Button2 = styled.button`
26 |
27 | color: blue;
28 | `
29 |
30 | // ⚠️ BAD INDENTATION ⚠️ (Generic Type)
31 | export const Button3 = styled.button`
32 |
33 | color: blue;
34 | `;
35 |
36 | // ⚠️ BAD INDENTATION ⚠️ (Inline Generic Type)
37 | export const Button4 = styled.button<{ a: boolean; b: string }>`
38 |
39 | color: blue;
40 | `;
41 |
42 |
--------------------------------------------------------------------------------
/src/utils/result.js:
--------------------------------------------------------------------------------
1 | exports.isCausedBySubstitution = (warning, line, interpolationLines) =>
2 | interpolationLines.some(({ start, end }) => {
3 | if (line > start && line < end) {
4 | // Inner interpolation lines must be
5 | return true
6 | } else if (line === start) {
7 | return ['value-list-max-empty-lines', 'comment-empty-line-before'].indexOf(warning.rule) >= 0
8 | } else if (line === end) {
9 | return ['comment-empty-line-before', 'indentation'].indexOf(warning.rule) >= 0
10 | } else {
11 | return false
12 | }
13 | })
14 |
15 | exports.getCorrectColumn = (taggedTemplateLocs, line, column) => {
16 | let c = column
17 |
18 | // Not consider multiple tagged literals exsit in the same line,
19 | // so we only add column offset of the first one
20 | taggedTemplateLocs.some(loc => {
21 | if (line === loc.start.line) {
22 | // Start column contains the back quote, so we need inscrese 1
23 | c += loc.start.column + 1 - loc.wrappedOffset
24 | return true
25 | }
26 | return false
27 | })
28 |
29 | return c
30 | }
31 |
--------------------------------------------------------------------------------
/test/emptyblock.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 |
4 | const processor = path.join(__dirname, '../src/index.js')
5 | const rules = {
6 | 'block-no-empty': true
7 | }
8 | const code = `import styled from 'styled-components';
9 | const DangerButton = styled(Button)\`\`;
10 | `
11 |
12 | describe('empty block', () => {
13 | let data
14 |
15 | beforeAll(done => {
16 | stylelint
17 | .lint({
18 | code,
19 | config: {
20 | processors: [processor],
21 | rules
22 | }
23 | })
24 | .then(result => {
25 | data = result
26 | done()
27 | })
28 | .catch(err => {
29 | data = err
30 | done()
31 | })
32 | })
33 |
34 | it('should have one result', () => {
35 | expect(data.results.length).toEqual(1)
36 | })
37 |
38 | it('should not have errored', () => {
39 | expect(data.errored).toEqual(false)
40 | })
41 |
42 | it('should not have any warnings', () => {
43 | expect(data.results[0].warnings.length).toEqual(0)
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/test/emptycode.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 |
4 | const processor = path.join(__dirname, '../src/index.js')
5 | const rules = {
6 | 'no-empty-source': true
7 | }
8 |
9 | describe('empty source', () => {
10 | let data
11 |
12 | beforeAll(done => {
13 | stylelint
14 | .lint({
15 | code: '',
16 | config: {
17 | processors: [processor],
18 | rules
19 | }
20 | })
21 | .then(result => {
22 | data = result
23 | done()
24 | })
25 | .catch(err => {
26 | data = err
27 | done()
28 | })
29 | })
30 |
31 | it('should have one result', () => {
32 | expect(data.results.length).toEqual(1)
33 | })
34 |
35 | it('should have errored', () => {
36 | expect(data.errored).toEqual(true)
37 | })
38 |
39 | it('should have one warning', () => {
40 | expect(data.results[0].warnings.length).toEqual(1)
41 | })
42 |
43 | it('should have the right line number', () => {
44 | expect(data.results[0].warnings[0].line).toEqual(1)
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Maximilian Stoiber
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/fixtures/real-world/Circle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 |
4 | // Valid indentation
5 | const animations = {
6 | spinnerCircle: keyframes`
7 | 0% {
8 | opacity: 0;
9 | }
10 |
11 | 100% {
12 | opacity: 1;
13 | }
14 | `
15 | }
16 |
17 | export default props => {
18 | const CirclePrimitive = styled.div`
19 | width: 100%;
20 | height: 100%;
21 | position: absolute;
22 | left: 0;
23 | top: 0;
24 | ${props.rotate &&
25 | `
26 | -webkit-transform: rotate(${props.rotate}deg);
27 | -ms-transform: rotate(${props.rotate}deg);
28 | transform: rotate(${props.rotate}deg);
29 | `}
30 |
31 | &:before {
32 | content: '';
33 | display: block;
34 | margin: 0 auto;
35 | width: 15%;
36 | height: 15%;
37 | background-color: #333;
38 | border-radius: 100%;
39 | animation: ${animations.spinnerCircle} 1.2s infinite ease-in-out both;
40 | ${props.delay &&
41 | `
42 | -webkit-animation-delay: ${props.delay}s;
43 | animation-delay: ${props.delay}s;
44 | `}
45 | }
46 | `
47 | return React.createElement(CirclePrimitive)
48 | }
49 |
--------------------------------------------------------------------------------
/src/utils/parse.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Parsing helpers
3 | */
4 |
5 | const parseImports = (node, currentNames) => {
6 | const names = { ...currentNames }
7 | const imports = node.specifiers.filter(
8 | specifier => specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportSpecifier'
9 | )
10 |
11 | imports.forEach(singleImport => {
12 | if (singleImport.imported) {
13 | // Is helper method
14 | names[singleImport.imported.name] = singleImport.local.name
15 | } else {
16 | // Is default import
17 | names.default = singleImport.local.name
18 | }
19 | })
20 |
21 | return names
22 | }
23 |
24 | const getSourceMap = (fullCSS, fragmentCSS, startInSource) => {
25 | const correction = {}
26 | // Save which line in the full CSS is which line in the source
27 | const fullCSSLength = fullCSS.split(/\n/).length
28 | const currentCSSLength = fragmentCSS.split(/\n/).length
29 | const currentCSSStart = fullCSSLength - currentCSSLength + 1
30 | // eslint-disable-next-line no-plusplus
31 | for (let i = 0; i < currentCSSLength + 1; i++) {
32 | correction[currentCSSStart + i] = startInSource + i
33 | }
34 | return correction
35 | }
36 |
37 | exports.parseImports = parseImports
38 | exports.getSourceMap = getSourceMap
39 |
--------------------------------------------------------------------------------
/test/fixtures/hard/invalid-indentation.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | // ⚠ 2 indentation errors ⚠
4 | const Comp = () => {
5 | const Button = styled.button`
6 | color: blue;
7 | background: red;
8 | display: block;
9 | `
10 |
11 | return Button
12 | }
13 |
14 | // ⚠ 2 indentation errors ⚠
15 | const Comp2 = () => {
16 | const InnerComp = () => {
17 | const Button = styled.button`
18 | color: blue;
19 | background: red;
20 | display: block;
21 | `
22 |
23 | return Button
24 | }
25 |
26 | return InnerComp()
27 | }
28 |
29 | // The below don't follow our specifications of keeping closing backtick on base indentation level
30 | // ⚠ 3 indentation errors ⚠
31 | const Comp3 = () => {
32 | const InnerComp = () => {
33 | const Button = styled.button`
34 | color: blue;
35 | background: red;
36 | display: block;
37 | `
38 |
39 | return Button
40 | }
41 |
42 | return InnerComp()
43 | }
44 |
45 | // ⚠ 3 indentation errors ⚠
46 | const Comp4 = () => {
47 | const InnerComp = () => {
48 | const Button = styled.button`
49 | color: blue;
50 | background: red;
51 | display: block;`
52 |
53 | return Button
54 | }
55 |
56 | return InnerComp()
57 | }
58 |
59 | // ⚠ 3 indentation errors ⚠
60 | const Comp5 = () => {
61 | const InnerComp = () => {
62 | const Button = styled.button`
63 | color: blue;
64 | background: red;
65 | display: block;
66 | `
67 |
68 | return Button
69 | }
70 |
71 | return InnerComp()
72 | }
73 |
--------------------------------------------------------------------------------
/test/garbage-css.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 | const slash = require('slash')
4 |
5 | const processor = path.join(__dirname, '../src/index.js')
6 | const rules = {
7 | 'block-no-empty': true,
8 | 'declaration-block-no-duplicate-properties': true,
9 | indentation: 2
10 | }
11 |
12 | describe('interpolation-tagging', () => {
13 | let fixture
14 | let data
15 |
16 | beforeEach(done => {
17 | stylelint
18 | .lint({
19 | files: [fixture],
20 | config: {
21 | processors: [processor],
22 | rules
23 | }
24 | })
25 | .then(result => {
26 | data = result
27 | done()
28 | })
29 | .catch(err => {
30 | data = err
31 | done()
32 | })
33 | })
34 |
35 | describe('valid', () => {
36 | beforeAll(() => {
37 | fixture = slash(path.join(__dirname, './fixtures/garbage-css/invalid-css.js'))
38 | })
39 |
40 | it('should have one result', () => {
41 | expect(data.results.length).toEqual(1)
42 | })
43 |
44 | it('should use the right file', () => {
45 | expect(slash(data.results[0].source)).toEqual(fixture)
46 | })
47 |
48 | it('should have errored', () => {
49 | expect(data.errored).toEqual(true)
50 | })
51 |
52 | it('should not have exactly one warning', () => {
53 | expect(data.results[0].warnings.length).toEqual(1)
54 | })
55 |
56 | it('should be a CssSyntaxError', () => {
57 | const warning = data.results[0].warnings[0]
58 | expect(warning.severity).toBe('error')
59 | expect(warning.rule).toBe('CssSyntaxError')
60 | expect(warning.text).toMatch('Unknown word')
61 | })
62 |
63 | it('should apply sourcemaps correctly', () => {
64 | const warning = data.results[0].warnings[0]
65 | expect(warning.line).toBe(4)
66 | expect(warning.column).toBe(3)
67 | })
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 | ## Environment
32 |
33 | `npx envinfo --system --binaries --npmPackages stylelint,styled-components,stylelint-processor-styled-components,stylelint-config-styled-components,stylelint-config-recommended --markdown --clipboard`
34 |
35 |
36 |
37 | ## Reproduction
38 |
39 |
40 | https://www.webpackbin.com/bins/-KeeZCr0xKfutOfOujxN
41 | https://codesandbox.io/s/rkmNRByE4
42 |
43 | ## Steps to reproduce
44 |
45 | ## Expected Behavior
46 |
47 | ## Actual Behavior
48 |
--------------------------------------------------------------------------------
/test/real-world.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Real world failures
3 | */
4 |
5 | const stylelint = require('stylelint')
6 | const path = require('path')
7 | const slash = require('slash')
8 |
9 | const processor = path.join(__dirname, '../src/index.js')
10 | const rules = {
11 | 'rule-empty-line-before': [
12 | 'always-multi-line',
13 | {
14 | except: ['first-nested'],
15 | ignore: ['after-comment']
16 | }
17 | ],
18 | 'block-no-empty': true,
19 | indentation: 2
20 | }
21 |
22 | describe('real world failures', () => {
23 | let fixture
24 | let data
25 |
26 | // NOTE beforeEach() runs _after_ the beforeAll() hooks of the describe() blocks, so `fixture`
27 | // will have the right path
28 | beforeEach(done => {
29 | stylelint
30 | .lint({
31 | files: [fixture],
32 | config: {
33 | processors: [processor],
34 | rules
35 | }
36 | })
37 | .then(result => {
38 | data = result
39 | done()
40 | })
41 | .catch(err => {
42 | data = err
43 | done()
44 | })
45 | })
46 |
47 | describe('Circle', () => {
48 | beforeAll(() => {
49 | fixture = slash(path.join(__dirname, './fixtures/real-world/Circle.js'))
50 | })
51 |
52 | it('should have one result', () => {
53 | expect(data.results.length).toEqual(1)
54 | })
55 |
56 | it('should use the right file', () => {
57 | expect(slash(data.results[0].source)).toEqual(fixture)
58 | })
59 |
60 | it('should not have errored', () => {
61 | expect(data.errored).toEqual(false)
62 | })
63 |
64 | it('should not have any warnings', () => {
65 | expect(data.results[0].warnings).toEqual([])
66 | })
67 | })
68 |
69 | describe('Line Numbers Report Correctly', () => {
70 | beforeAll(() => {
71 | fixture = slash(path.join(__dirname, './fixtures/real-world/LineNumbersReportedAccurate.js'))
72 | })
73 |
74 | it('should have one result', () => {
75 | expect(data.results.length).toEqual(1)
76 | })
77 |
78 | it('should use the right file', () => {
79 | expect(slash(data.results[0].source)).toEqual(fixture)
80 | })
81 |
82 | it('should not have errored', () => {
83 | expect(data.errored).toEqual(true)
84 | })
85 |
86 | it('should identify the line number correctly', () => {
87 | const errorLine = 20
88 | expect(data.results[0].warnings[0].line).toEqual(errorLine)
89 | })
90 | })
91 | })
92 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stylelint-processor-styled-components",
3 | "version": "1.10.0",
4 | "description": "A stylelint processor for styled-components",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "lint:eslint": "eslint .",
8 | "lint:prettier": "prettier --list-different \"./**/*.js\"",
9 | "prettier": "prettier --write \"./**/*.js\"",
10 | "test": "jest"
11 | },
12 | "jest": {
13 | "testMatch": [
14 | "/test/*.test.js"
15 | ],
16 | "setupFilesAfterEnv": [
17 | "./test/jest-setup.js"
18 | ]
19 | },
20 | "husky": {
21 | "hooks": {
22 | "pre-commit": "lint-staged"
23 | }
24 | },
25 | "lint-staged": {
26 | "**/*.js": [
27 | "npm run lint:eslint --fix",
28 | "npm run lint:prettier --write"
29 | ]
30 | },
31 | "repository": {
32 | "type": "git",
33 | "url": "git+https://github.com/styled-components/stylelint-processor-styled-components.git"
34 | },
35 | "keywords": [
36 | "stylelint",
37 | "processor",
38 | "stylelint-processor",
39 | "styled-components",
40 | "lint"
41 | ],
42 | "author": "Max Stoiber",
43 | "license": "MIT",
44 | "bugs": {
45 | "url": "https://github.com/styled-components/stylelint-processor-styled-components/issues"
46 | },
47 | "homepage": "https://github.com/styled-components/stylelint-processor-styled-components#readme",
48 | "greenkeeper": {
49 | "ignore": [
50 | "eslint",
51 | "eslint-plugin-import",
52 | "eslint-config-airbnb-base",
53 | "husky",
54 | "jest",
55 | "lint-staged",
56 | "lodash",
57 | "prettier"
58 | ]
59 | },
60 | "devDependencies": {
61 | "@babel/core": "^7.8.3",
62 | "babel-core": "^7.0.0-bridge.0",
63 | "eslint": "^6.8.0",
64 | "eslint-config-airbnb-base": "^14.0.0",
65 | "eslint-config-prettier": "^6.10.0",
66 | "eslint-plugin-import": "^2.20.0",
67 | "eslint-plugin-prettier": "^3.1.2",
68 | "husky": "^4.2.1",
69 | "jest": "^25.1.0",
70 | "lint-staged": "^10.0.3",
71 | "lodash": "^4.17.15",
72 | "prettier": "^1.19.1",
73 | "slash": "^3.0.0",
74 | "stylelint": "^13.0.0",
75 | "typescript": "~3.7.5"
76 | },
77 | "peerDependency": {
78 | "stylelint-config-styled-components": "^0.1.1",
79 | "stylelint": ">= 10"
80 | },
81 | "dependencies": {
82 | "@babel/parser": "^7.8.3",
83 | "@babel/traverse": "^7.8.3",
84 | "micromatch": "^4.0.2",
85 | "postcss": "^7.0.26"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/test/nofiles.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 |
4 | const code = `import styled from 'styled-components';
5 | const StyledTable = styled(StyledTableBase)\`
6 | color: red;
7 | \``
8 | const processor = path.join(__dirname, '../src/index.js')
9 | const rules = {
10 | indentation: 2
11 | }
12 |
13 | describe('no files', () => {
14 | let codeFilename
15 | let data
16 |
17 | // NOTE beforeEach() runs _after_ the beforeAll() hooks of the describe() blocks
18 | beforeEach(done => {
19 | stylelint
20 | .lint({
21 | code,
22 | codeFilename,
23 | config: {
24 | processors: [processor],
25 | rules
26 | }
27 | })
28 | .then(result => {
29 | data = result
30 | done()
31 | })
32 | .catch(err => {
33 | // eslint-disable-next-line
34 | console.log(err)
35 | data = err
36 | done()
37 | })
38 | })
39 |
40 | describe('no codeFilename', () => {
41 | beforeAll(() => {
42 | codeFilename = undefined
43 | })
44 |
45 | it('should have one result', () => {
46 | expect(data.results.length).toEqual(1)
47 | })
48 |
49 | it('should use the right file', () => {
50 | // Every new linting that occurs in beforeEach() will increase the id
51 | expect(data.results[0].source).toEqual('')
52 | })
53 |
54 | it('should have errored', () => {
55 | expect(data.errored).toEqual(true)
56 | })
57 |
58 | it('should have one warning', () => {
59 | expect(data.results[0].warnings.length).toEqual(1)
60 | })
61 |
62 | it('should have the right line number', () => {
63 | expect(data.results[0].warnings[0].line).toEqual(3)
64 | })
65 | })
66 |
67 | describe('has codeFilename', () => {
68 | beforeAll(() => {
69 | codeFilename = 'somefile.js'
70 | })
71 |
72 | it('should have one result', () => {
73 | expect(data.results.length).toEqual(1)
74 | })
75 |
76 | it('should use the right file', () => {
77 | expect(data.results[0].source).toEqual(path.resolve(process.cwd(), codeFilename))
78 | })
79 |
80 | it('should have errored', () => {
81 | expect(data.errored).toEqual(true)
82 | })
83 |
84 | it('should have one warning', () => {
85 | expect(data.results[0].warnings.length).toEqual(1)
86 | })
87 |
88 | it('should have the right line number', () => {
89 | expect(data.results[0].warnings[0].line).toEqual(3)
90 | })
91 | })
92 | })
93 |
--------------------------------------------------------------------------------
/test/fixtures/interpolations/valid.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const color = 'red'
4 |
5 | // Normal styled component
6 | const Button = styled.button`
7 | display: block;
8 | color: ${color};
9 | background: blue;
10 | `
11 |
12 | const Box = styled.div`
13 | display: block;
14 | color: ${color};
15 | background: blue;
16 | `
17 |
18 | // Tagname notation
19 | const Button2 = styled('button')`
20 | display: block;
21 | color: ${color};
22 | background: blue;
23 | `
24 |
25 | const Box2 = styled('div')`
26 | display: block;
27 | color: ${color};
28 | background: blue;
29 | `
30 |
31 | // Component Notation
32 | const Button3 = styled(Button2)`
33 | display: block;
34 | color: ${color};
35 | background: blue;
36 | `
37 |
38 | const Box3 = styled(Box2)`
39 | display: block;
40 | color: ${color};
41 | background: blue;
42 | `
43 |
44 | // Multiline
45 | const Button4 = styled.button`
46 | display: block;
47 | ${`
48 | color: blue;
49 | `}
50 | background: blue;
51 | `
52 |
53 | // Conditional
54 | const cond = true
55 | const Button5 = styled.button`
56 | display: block;
57 | ${cond &&
58 | `
59 | color: blue;
60 | `}
61 | background: blue;
62 | `
63 |
64 | // Conditional
65 | const cond2 = false
66 | const Button6 = styled.button`
67 | display: block;
68 | ${cond2 &&
69 | `
70 | color: ${cond2};
71 | `}
72 | background: blue;
73 | `
74 |
75 | // multi interpolations within a property
76 | const borderWidth = '1px'
77 | const borderStyle = 'solid'
78 | const Button7 = styled.button`
79 | width: 20px;
80 | border: ${borderWidth} ${borderStyle} ${color};
81 | `
82 |
83 | // Several interpolation statements in a block
84 | const Button8 = styled.button`
85 | ${`display: block;`}
86 | ${`color: ${color};`}
87 | ${`background: blue;`}
88 | `
89 | // Simple interpolations in one-line css
90 | const display = 'block'
91 | const colorExpression = 'color: red;'
92 | const Button91 = styled.button`
93 | display: ${display}; ${colorExpression}
94 | `
95 |
96 | // Complex interpolations in one-line css
97 | const backgroundExpression = 'background: blue;'
98 | const Button92 = styled.button`
99 | display: ${display}; ${colorExpression} ${backgroundExpression}
100 | `
101 | // Interpolations in nested blocks
102 | const Button10 = styled.button`
103 | span {
104 | ${'display: block;'}
105 | }
106 | `;
107 |
108 | // Interpolations after comments
109 | const Button11 = styled.button`
110 | display: block; /* stylelint-disable-line */
111 | ${colorExpression}
112 | `
113 |
114 | const Button12 = styled.button`
115 | display: block;
116 | /* stylelint-disable */
117 | ${colorExpression}
118 | `
119 |
120 | const Button13 = styled.button`
121 | display: block;
122 | /*
123 | multiline comment with "*" and "/"
124 | */
125 | ${colorExpression}
126 | `
127 |
128 | const Button14 = styled.button`
129 | display: block;
130 | /**
131 | * JSDoc style comment
132 | */
133 | ${colorExpression}
134 | `
135 |
--------------------------------------------------------------------------------
/test/interpolation-tagging.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 | const slash = require('slash')
4 |
5 | const processor = path.join(__dirname, '../src/index.js')
6 | const rules = {
7 | 'block-no-empty': true,
8 | 'declaration-block-no-duplicate-properties': true,
9 | 'no-duplicate-selectors': true,
10 | indentation: 2
11 | }
12 |
13 | describe('interpolation-tagging', () => {
14 | let fixture
15 | let data
16 |
17 | beforeEach(done => {
18 | stylelint
19 | .lint({
20 | files: [fixture],
21 | config: {
22 | processors: [processor],
23 | rules
24 | }
25 | })
26 | .then(result => {
27 | data = result
28 | done()
29 | })
30 | .catch(err => {
31 | data = err
32 | done()
33 | })
34 | })
35 |
36 | describe('valid', () => {
37 | beforeAll(() => {
38 | fixture = slash(path.join(__dirname, './fixtures/interpolation-tagging/valid.js'))
39 | })
40 |
41 | it('should have one result', () => {
42 | expect(data.results.length).toEqual(1)
43 | })
44 |
45 | it('should use the right file', () => {
46 | expect(slash(data.results[0].source)).toEqual(fixture)
47 | })
48 |
49 | it('should not have errored', () => {
50 | expect(data.errored).toEqual(false)
51 | })
52 |
53 | it('should not have any warnings', () => {
54 | expect(data.results[0].warnings.length).toEqual(0)
55 | })
56 | })
57 |
58 | describe('invalid tag', () => {
59 | beforeAll(() => {
60 | fixture = slash(path.join(__dirname, './fixtures/interpolation-tagging/invalid-tag.js'))
61 | })
62 |
63 | it('should return error', () => {
64 | expect(data.errored).toEqual(true)
65 | })
66 |
67 | it('should have exactly one warning', () => {
68 | expect(data.results[0].warnings.length).toBe(1)
69 | })
70 |
71 | it('should throw correct error', () => {
72 | const warning = data.results[0].warnings[0]
73 | expect(warning.line).toBe(5)
74 | expect(warning.column).toBe(4)
75 | expect(warning.severity).toBe('error')
76 | expect(warning.text).toMatch(/Styled Components.*invalid sc- tag/)
77 | })
78 | })
79 |
80 | describe('invalid custom tag', () => {
81 | beforeAll(() => {
82 | fixture = slash(path.join(__dirname, './fixtures/interpolation-tagging/invalid-custom.js'))
83 | })
84 |
85 | it('should return error', () => {
86 | expect(data.errored).toEqual(true)
87 | })
88 |
89 | it('should have exactly one warning', () => {
90 | expect(data.results[0].warnings.length).toBe(1)
91 | })
92 |
93 | it('should throw correct error', () => {
94 | const warning = data.results[0].warnings[0]
95 | expect(warning.line).toBe(5)
96 | expect(warning.column).toBe(4)
97 | expect(warning.severity).toBe('error')
98 | expect(warning.text).toMatch(/We were unable to parse.*Styled Components/)
99 | })
100 | })
101 | })
102 |
--------------------------------------------------------------------------------
/src/utils/styled.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const { isTaggedTemplateLiteral } = require('./tagged-template-literal')
3 |
4 | /**
5 | * Check if something is a styled-components import declaration
6 | */
7 | const isStyledImport = (node, moduleName) =>
8 | node.type === 'ImportDeclaration' && path.basename(node.source.value) === moduleName
9 |
10 | /**
11 | * Check if something is a styled shorthand call
12 | * e.g. styled.div``
13 | *
14 | * TODO Lint that the tagname exists
15 | */
16 | const isStyledShorthand = (node, styledVariableName) =>
17 | // Check that it's an object
18 | node.tag &&
19 | node.tag.object &&
20 | // Check that the object matches the imported name
21 | node.tag.object.name === styledVariableName &&
22 | // Check that a property exists, otherwise it's just styled
23 | // without any call
24 | node.tag.property
25 |
26 | /**
27 | * Check if a node is a styld call
28 | * e.g. styled(Component)`` or styled('tagname')``
29 | */
30 | const isStyledCall = (node, styledVariableName) =>
31 | // Check that it's a function call
32 | node.tag &&
33 | node.tag.callee &&
34 | // And that the function name matches the imported name
35 | node.tag.callee.name === styledVariableName
36 |
37 | /**
38 | * Check if it has a .attrs postfix which we in that case handle specially
39 | */
40 | const hasAttrsCall = node =>
41 | // Check that it's a function call
42 | node.tag &&
43 | node.tag.callee &&
44 | // Check that the last member of the call is attrs
45 | node.tag.callee.property &&
46 | node.tag.callee.property.name === 'attrs'
47 |
48 | // We don't need the checks here as they were checked in hasAttrsCall
49 | const getAttrsObject = node => node.tag.callee.object
50 |
51 | /**
52 | * Check if something is a styled component call
53 | */
54 | const isStyled = (node, styledVariableName) =>
55 | isTaggedTemplateLiteral(node) &&
56 | (isStyledCall(node, styledVariableName) || isStyledShorthand(node, styledVariableName))
57 |
58 | /**
59 | * Check if it is a .extend call and we pretty reasonable assume that any TTL that ends
60 | * in a .extend must be a styled components call as there is no way to check if it was
61 | * called on a Styled Component
62 | */
63 | const isExtendCall = node => node.tag && node.tag.property && node.tag.property.name === 'extend'
64 |
65 | /**
66 | * Check if something is a call to one of our helper methods
67 | *
68 | * Returns either a string (the name of the helper) or false
69 | */
70 | const isHelper = (node, importedNames) => {
71 | if (!isTaggedTemplateLiteral(node)) return false
72 | let helper
73 | Object.keys(importedNames).forEach(name => {
74 | if (importedNames[name] === node.tag.name) {
75 | helper = name
76 | // eslint-disable-next-line no-useless-return
77 | return
78 | }
79 | })
80 | return helper || false
81 | }
82 |
83 | exports.isStyledImport = isStyledImport
84 | exports.isStyledShorthand = isStyledShorthand
85 | exports.isStyledCall = isStyledCall
86 | exports.isStyled = isStyled
87 | exports.isHelper = isHelper
88 | exports.hasAttrsCall = hasAttrsCall
89 | exports.getAttrsObject = getAttrsObject
90 | exports.isExtendCall = isExtendCall
91 |
--------------------------------------------------------------------------------
/test/inject-global.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 | const slash = require('slash')
4 |
5 | const processor = path.join(__dirname, '../src/index.js')
6 | const rules = {
7 | indentation: 2,
8 | 'selector-max-compound-selectors': 1
9 | }
10 |
11 | describe('inject-global', () => {
12 | describe('using spaces', () => {
13 | let fixture
14 | let data
15 |
16 | // NOTE beforeEach() runs _after_ the beforeAll() hooks of the describe() blocks, so `fixture`
17 | // will have the right path
18 | beforeEach(done => {
19 | stylelint
20 | .lint({
21 | files: [fixture],
22 | config: {
23 | processors: [processor],
24 | rules
25 | }
26 | })
27 | .then(result => {
28 | data = result
29 | done()
30 | })
31 | .catch(err => {
32 | // eslint-disable-next-line
33 | console.log(err)
34 | data = err
35 | done()
36 | })
37 | })
38 |
39 | describe('valid fixtures', () => {
40 | beforeAll(() => {
41 | fixture = slash(path.join(__dirname, './fixtures/inject-global/valid-spaces.js'))
42 | })
43 |
44 | it('should have one result', () => {
45 | expect(data.results.length).toEqual(1)
46 | })
47 |
48 | it('should use the right file', () => {
49 | expect(slash(data.results[0].source)).toEqual(fixture)
50 | })
51 |
52 | it('should not have errored', () => {
53 | expect(data.errored).toEqual(false)
54 | })
55 |
56 | it('should not have any warnings', () => {
57 | expect(data.results[0].warnings.length).toEqual(0)
58 | })
59 | })
60 | })
61 |
62 | describe('using tabs', () => {
63 | let fixture
64 | let data
65 |
66 | // NOTE beforeEach() runs _after_ the beforeAll() hooks of the describe() blocks, so `fixture`
67 | // will have the right path
68 | beforeEach(done => {
69 | stylelint
70 | .lint({
71 | files: [fixture],
72 | config: {
73 | processors: [processor],
74 | rules: { ...rules, indentation: 'tab' }
75 | }
76 | })
77 | .then(result => {
78 | data = result
79 | done()
80 | })
81 | .catch(err => {
82 | // eslint-disable-next-line
83 | console.log(err)
84 | data = err
85 | done()
86 | })
87 | })
88 |
89 | describe('valid fixtures', () => {
90 | beforeAll(() => {
91 | fixture = slash(path.join(__dirname, './fixtures/inject-global/valid-tabs.js'))
92 | })
93 |
94 | it('should have one result', () => {
95 | expect(data.results.length).toEqual(1)
96 | })
97 |
98 | it('should use the right file', () => {
99 | expect(slash(data.results[0].source)).toEqual(fixture)
100 | })
101 |
102 | it('should not have errored', () => {
103 | expect(data.errored).toEqual(false)
104 | })
105 |
106 | it('should not have any warnings', () => {
107 | expect(data.results[0].warnings.length).toEqual(0)
108 | })
109 | })
110 | })
111 | })
112 |
--------------------------------------------------------------------------------
/test/interpolations.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 | const slash = require('slash')
4 |
5 | const processor = path.join(__dirname, '../src/index.js')
6 | const rules = {
7 | 'block-no-empty': true,
8 | 'comment-empty-line-before': 'always',
9 | 'declaration-block-no-duplicate-properties': true,
10 | 'value-list-max-empty-lines': 0,
11 | 'max-empty-lines': 1,
12 | indentation: 2
13 | }
14 |
15 | describe('interpolations', () => {
16 | let fixture
17 | let data
18 |
19 | beforeEach(done => {
20 | stylelint
21 | .lint({
22 | files: [fixture],
23 | config: {
24 | processors: [processor],
25 | rules
26 | }
27 | })
28 | .then(result => {
29 | data = result
30 | done()
31 | })
32 | .catch(err => {
33 | data = err
34 | done()
35 | })
36 | })
37 |
38 | describe('valid', () => {
39 | beforeAll(() => {
40 | fixture = slash(path.join(__dirname, './fixtures/interpolations/valid.js'))
41 | })
42 |
43 | it('should have one result', () => {
44 | expect(data.results.length).toEqual(1)
45 | })
46 |
47 | it('should use the right file', () => {
48 | expect(slash(data.results[0].source)).toEqual(fixture)
49 | })
50 |
51 | it('should not have errored', () => {
52 | expect(data.errored).toEqual(false)
53 | })
54 |
55 | it('should not have any warnings', () => {
56 | expect(data.results[0].warnings.length).toEqual(0)
57 | })
58 | })
59 |
60 | describe('invalid interpolations', () => {
61 | beforeAll(() => {
62 | fixture = slash(path.join(__dirname, './fixtures/interpolations/invalid.js'))
63 | })
64 |
65 | it('should have one result', () => {
66 | expect(data.results.length).toEqual(1)
67 | })
68 |
69 | it('should use the right file', () => {
70 | expect(slash(data.results[0].source)).toEqual(fixture)
71 | })
72 |
73 | it('should have errored', () => {
74 | expect(data.errored).toEqual(true)
75 | })
76 |
77 | it('should have warnings', () => {
78 | expect(data.results[0].warnings.length).toEqual(1)
79 | })
80 |
81 | it('should have an indentation as the first warning', () => {
82 | expect(data.results[0].warnings[0].rule).toEqual('indentation')
83 | })
84 |
85 | it('should have the indentation warning in the right line', () => {
86 | expect(data.results[0].warnings[0].line).toEqual(16)
87 | })
88 | })
89 |
90 | describe('complex interpolations', () => {
91 | beforeAll(() => {
92 | fixture = slash(path.join(__dirname, './fixtures/interpolations/complex.js'))
93 | })
94 |
95 | it('should have one result', () => {
96 | expect(data.results.length).toEqual(1)
97 | })
98 |
99 | it('should use the right file', () => {
100 | expect(slash(data.results[0].source)).toEqual(fixture)
101 | })
102 |
103 | it('should not have errored', () => {
104 | expect(data.errored).toEqual(false)
105 | })
106 |
107 | it('should not have any warnings', () => {
108 | expect(data.results[0].warnings.length).toEqual(0)
109 | })
110 |
111 | it('should not result in a CssSyntaxError', () => {
112 | const warning = data.results[0].warnings[0] && data.results[0].warnings[0].rule
113 |
114 | expect(warning).not.toEqual('CssSyntaxError')
115 | })
116 | })
117 | })
118 |
--------------------------------------------------------------------------------
/test/typescript.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 | const slash = require('slash')
4 |
5 | const processor = path.join(__dirname, '../src/index.js')
6 | const rules = {
7 | 'block-no-empty': true,
8 | indentation: 2,
9 | 'rule-empty-line-before': [
10 | 'always-multi-line',
11 | {
12 | except: ['first-nested'],
13 | ignore: ['after-comment']
14 | }
15 | ]
16 | }
17 | const doLint = (fixture, done) =>
18 | stylelint
19 | .lint({
20 | files: [fixture],
21 | config: {
22 | processors: [processor],
23 | rules
24 | }
25 | })
26 | .then(result => result)
27 | .catch(err => {
28 | // eslint-disable-next-line
29 | console.log(err)
30 | done()
31 | return err
32 | })
33 |
34 | describe('Typescript files, both TS and TSX should parse and report any errors correctly', () => {
35 | it('should parse styled components code in TS files and report correctly the errors encountered', done => {
36 | const fixture = slash(path.join(__dirname, './fixtures/typescript/ts-syntax-invalid.ts'))
37 | doLint(fixture, done).then(data => {
38 | expect(data.results.length).toEqual(1)
39 | expect(data.results[0].warnings.length).toEqual(7)
40 | expect(data.results[0].warnings[0].rule).toEqual('block-no-empty')
41 | expect(data.results[0].warnings[1].rule).toEqual('indentation')
42 | expect(data.results[0].warnings[2].rule).toEqual('indentation')
43 | expect(data.results[0].warnings[3].rule).toEqual('indentation')
44 | expect(data.results[0].warnings[4].rule).toEqual('indentation')
45 | expect(data.results[0].warnings[5].rule).toEqual('indentation')
46 | expect(data.results[0].warnings[6].rule).toEqual('indentation')
47 | done()
48 | })
49 | })
50 |
51 | it('should not report errors when there are NOT any in a typescript files', done => {
52 | const fixture = slash(path.join(__dirname, './fixtures/typescript/ts-syntax-valid.ts'))
53 | doLint(fixture, done).then(data => {
54 | expect(data.results.length).toEqual(1)
55 | expect(data.results[0].warnings.length).toEqual(0)
56 | done()
57 | })
58 | })
59 |
60 | it('should report errors when parsing typescript files with SyntaxError', done => {
61 | const fixture = slash(path.join(__dirname, './fixtures/typescript/ts-syntax-parse-error.ts'))
62 | doLint(fixture, done).then(data => {
63 | expect(data.results.length).toEqual(1)
64 | expect(data.results[0].parseErrors.length).toEqual(1)
65 | expect(data.results[0].parseErrors[0].rule).toEqual('parseError')
66 | done()
67 | })
68 | })
69 |
70 | it('should report errors in TSX files(typescript + JSX)', done => {
71 | const fixture = slash(path.join(__dirname, './fixtures/typescript/ts-syntax-jsx-invalid.tsx'))
72 | doLint(fixture, done).then(data => {
73 | expect(data.results.length).toEqual(1)
74 | expect(data.results[0].warnings.length).toEqual(1)
75 | expect(data.results[0].warnings[0].rule).toEqual('indentation')
76 | done()
77 | })
78 | })
79 |
80 | it('should ignore errors raised by Stylelint in files without styled components(no-empty-source)', done => {
81 | const fixture = slash(
82 | path.join(__dirname, './fixtures/typescript/ts-syntax-no-styled-components.tsx')
83 | )
84 | doLint(fixture, done).then(data => {
85 | expect(data.results.length).toEqual(1)
86 | expect(data.results[0].warnings.length).toEqual(0)
87 | done()
88 | })
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/src/parsers/index.js:
--------------------------------------------------------------------------------
1 | const traverse = require('@babel/traverse').default
2 |
3 | const estreeParse = require('./babylon-parser')
4 |
5 | const {
6 | isStyled,
7 | isHelper,
8 | isStyledImport,
9 | hasAttrsCall,
10 | getAttrsObject,
11 | isExtendCall
12 | } = require('../utils/styled')
13 | const {
14 | wrapSelector,
15 | wrapKeyframes,
16 | fixIndentation,
17 | removeBaseIndentation,
18 | isStylelintComment
19 | } = require('../utils/general')
20 |
21 | const {
22 | getTaggedTemplateLiteralContent: getTTLContent
23 | } = require('../utils/tagged-template-literal.js')
24 | const { parseImports, getSourceMap } = require('../utils/parse')
25 |
26 | const processStyledComponentsFile = (ast, absolutePath, options) => {
27 | const extractedCSS = []
28 | let ignoreRuleComments = []
29 | let importedNames = {
30 | default: 'styled',
31 | css: 'css',
32 | keyframes: 'keyframes',
33 | injectGlobal: 'injectGlobal',
34 | createGlobalStyle: 'createGlobalStyle'
35 | }
36 | let sourceMap = {}
37 | const taggedTemplateLocs = []
38 | const interpolationLines = []
39 | traverse(ast, {
40 | noScope: true,
41 | enter({ node }) {
42 | if (node.type !== 'Program' && node.leadingComments) {
43 | node.leadingComments.forEach(comment => {
44 | if (isStylelintComment(comment.value)) {
45 | ignoreRuleComments.push(`/*${comment.value}*/`)
46 | }
47 | })
48 | }
49 | if (isStyledImport(node, options.moduleName)) {
50 | importedNames = parseImports(node)
51 | return
52 | }
53 | const helper = !options.strict
54 | ? isHelper(node, importedNames)
55 | : isHelper(node, [importedNames[options.importName]])
56 | const processedNode = { ...node }
57 | if (hasAttrsCall(node)) {
58 | processedNode.tag = getAttrsObject(node)
59 | }
60 | if (
61 | !helper &&
62 | !isStyled(processedNode, importedNames[options.importName]) &&
63 | !isExtendCall(node)
64 | ) {
65 | return
66 | }
67 | const content = getTTLContent(node, absolutePath)
68 | if (!content) return
69 |
70 | const fixedContent = fixIndentation(content).text
71 | let wrappedContent
72 | switch (helper) {
73 | case 'keyframes':
74 | // wrap it in a @keyframes block
75 | wrappedContent = wrapKeyframes(fixedContent)
76 | break
77 |
78 | case 'injectGlobal':
79 | // Don't wrap it as it goes in global scope, but put it to
80 | // base line to avoid indentation errors
81 | wrappedContent = removeBaseIndentation(fixedContent)
82 | break
83 |
84 | default:
85 | // Wrap it in a dummy selector as this is what Styled Components would do
86 | wrappedContent = wrapSelector(fixedContent)
87 | }
88 | const stylelintCommentsAdded =
89 | ignoreRuleComments.length > 0
90 | ? `${ignoreRuleComments.join('\n')}\n${wrappedContent}`
91 | : wrappedContent
92 | extractedCSS.push(stylelintCommentsAdded)
93 | sourceMap = Object.assign(
94 | sourceMap,
95 | getSourceMap(extractedCSS.join('\n'), wrappedContent, processedNode.quasi.loc.start.line)
96 | )
97 | // Save `loc` of template literals
98 | taggedTemplateLocs.push({
99 | wrappedOffset: wrappedContent.indexOf(fixedContent),
100 | start: node.quasi.loc.start
101 | })
102 | // Save dummy interpolation lines
103 | node.quasi.expressions.forEach((expression, i) => {
104 | interpolationLines.push({
105 | start: node.quasi.quasis[i].loc.end.line,
106 | end: node.quasi.quasis[i + 1].loc.start.line
107 | })
108 | })
109 |
110 | /**
111 | * All queued comments have been added to the file so we don't need to, and actually shouldn't
112 | * add them to the file more than once
113 | */
114 | ignoreRuleComments = []
115 | }
116 | })
117 |
118 | return {
119 | extractedCSS: extractedCSS.join('\n'),
120 | taggedTemplateLocs,
121 | interpolationLines,
122 | sourceMap
123 | }
124 | }
125 |
126 | module.exports = (input, absolutePath, options) => {
127 | const typedParser =
128 | absolutePath.endsWith('.ts') || absolutePath.endsWith('.tsx') ? 'typescript' : 'flow'
129 | const ast = estreeParse(typedParser, options.parserPlugins)(input)
130 | return processStyledComponentsFile(ast, absolutePath, options)
131 | }
132 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const micromatch = require('micromatch')
3 | const parse = require('./parsers/index')
4 | const { isCausedBySubstitution, getCorrectColumn } = require('./utils/result')
5 |
6 | let inputId = 1
7 |
8 | // Make sure that state for particular path will be cleaned before each run
9 | // module may be kept in memory when used with vscode-stylelint
10 | const taggedTemplateLocsMap = {}
11 | const interpolationLinesMap = {}
12 | const sourceMapsCorrections = {}
13 | const errorWasThrown = {}
14 | const DEFAULT_OPTIONS = {
15 | moduleName: 'styled-components',
16 | importName: 'default',
17 | strict: false,
18 | ignoreFiles: []
19 | }
20 |
21 | const realProcessor = options => ({
22 | // Get string for stylelint to lint
23 | code(input, filepath) {
24 | let absolutePath
25 | if (filepath) {
26 | absolutePath = path.resolve(process.cwd(), filepath)
27 | } else {
28 | absolutePath = ``
29 | inputId += 1
30 | }
31 |
32 | const fileIsIgnored = micromatch(filepath, options.ignoreFiles).length
33 |
34 | if (fileIsIgnored) return input
35 |
36 | try {
37 | const { extractedCSS, interpolationLines, taggedTemplateLocs, sourceMap } = parse(
38 | input,
39 | absolutePath,
40 | options
41 | )
42 | // Save `loc` of template literals
43 | taggedTemplateLocsMap[absolutePath] = taggedTemplateLocs
44 | // Save dummy interpolation lines
45 | interpolationLinesMap[absolutePath] = interpolationLines
46 | // Save source location
47 | sourceMapsCorrections[absolutePath] = sourceMap
48 | // Clean saved errors
49 | delete errorWasThrown[absolutePath]
50 |
51 | return extractedCSS
52 | } catch (e) {
53 | // Always save the error
54 | errorWasThrown[absolutePath] = e
55 | // Incorrect interpolations will throw CssSyntaxError and they'll be handled by stylelint
56 | // so we can throw it out but not for others
57 | if (e.name === 'CssSyntaxError') {
58 | throw e
59 | }
60 | return ''
61 | }
62 | },
63 | // Fix sourcemaps
64 | result(stylelintResult, filepath) {
65 | const err = errorWasThrown[filepath]
66 | const fileIsIgnored = micromatch(filepath, options.ignoreFiles).length
67 |
68 | if (fileIsIgnored) return stylelintResult
69 |
70 | if (err) {
71 | if (err.name === 'CssSyntaxError') {
72 | // We threw an error ourselves, in this case we have already put correct
73 | // line/column numbers so no source maps are needed
74 | // (and would actually break the line numbers)
75 | return stylelintResult
76 | } else {
77 | // For other errors, wrap them into the result
78 | return {
79 | ...stylelintResult,
80 | errored: true,
81 | parseErrors: [
82 | {
83 | line: err.loc && err.loc.line,
84 | column: err.loc && err.loc.column,
85 | rule: 'parseError',
86 | severity: 'error',
87 | text: `${err.message}`
88 | }
89 | ]
90 | }
91 | }
92 | }
93 | const taggedTemplateLocs = taggedTemplateLocsMap[filepath] || []
94 | const interpolationLines = interpolationLinesMap[filepath] || []
95 | const lineCorrection = sourceMapsCorrections[filepath]
96 | const warnings = stylelintResult.warnings
97 | .filter(
98 | warning =>
99 | // Filter false-positive warnings generated by interpolations substitution
100 | !isCausedBySubstitution(warning, lineCorrection[warning.line], interpolationLines)
101 | )
102 | .map(warning => ({
103 | ...warning,
104 | // Replace "brace" with "backtick" in warnings, e.g.
105 | // "Unexpected empty line before closing backtick" (instead of "brace")
106 | text: warning.text.replace(/brace/, 'backtick'),
107 | line: lineCorrection[warning.line] || warning.line,
108 | column: getCorrectColumn(
109 | taggedTemplateLocs,
110 | lineCorrection[warning.line] || warning.line,
111 | warning.column
112 | )
113 | }))
114 |
115 | const result = { ...stylelintResult, warnings }
116 | // Undo `errored` if no warnings with error severity any more
117 | if (result.errored && !warnings.some(warning => warning.severity === 'error')) {
118 | delete result.errored
119 | }
120 | return result
121 | }
122 | })
123 |
124 | module.exports = options => realProcessor({ ...DEFAULT_OPTIONS, ...options })
125 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### 1.10.0
2 | * upgrade @babel/core to ^7.8.3 to support new ECMAScript 2020 features by default, see #290
3 | * upgrade stylelint to 13 and other dependencies/devDependencies
4 |
5 | ### 1.9.0
6 | * upgrade stylelint to 12
7 |
8 | ### 1.8.0
9 | * support new option `ignoreFiles`, which allows to skip files to be linted
10 |
11 | ### 1.7.0
12 | * upgrade stylelint to 10, and drop supports for Node < 8 as well
13 |
14 | ### 1.6.0
15 | * avoid `block-no-empty` error
16 | * support new option `strict`, which allows to only parse `importName`
17 |
18 | ### 1.5.2
19 | * upgrade postcss@7
20 |
21 | ### 1.5.1
22 | * fix column number if it is at the first line of interpolation
23 | * clean module level variables before each running
24 |
25 | ### 1.5.0
26 | * fix line number is undefined if source is empty
27 | * support new option `parserPlugins`, which allows to specify custom babel plugins to parse
28 |
29 | ### 1.4.0
30 | * upgrade babylon parser to @babel/parser
31 | * able to show parse errors to users
32 | * support new option `importName`, which allows to import wrapper components object from other names
33 | * filter redundant warnings introduced by v1.2.0
34 | * more intelligent guessing for interpolations
35 |
36 | ### 1.3.2
37 | * added support for optional chaining to our babylon parser
38 | * various minor development improvements that shouldn't affect the product
39 |
40 | ### 1.3.1
41 | * FIX: Interpolations following multiline comments no longer incorrectly error
42 | * README UPDATE: Recommend the recommended instead of standard syntax and no longer recommend the syntax config option for stylelint
43 |
44 | ### 1.3.0
45 | * FIX: Moved to using babylon's new parser that handles typescript, this will fix the problems we had with interpolation tags not being useable in Typescript
46 | * FIX: We now properly handle applying sourcemaps to CSS syntax errors and displaying correct line numbers for errors, Stylelint v9+ is required for this fix though as the biggest part of the fix was on their end
47 | * Stylelint v9+ was added as a peerdependency due to above reasons, no promises are made with this version if you do not update your Stylelint version
48 |
49 | ### 1.2.2
50 | * FIX: Correctly remove base indentation from injectGlobal when using tabs
51 |
52 | ### 1.2.1
53 | * FIX: Don't throw errors on using processor without filename (with Node.js API / CLI)
54 |
55 | ### 1.2.0
56 | * Substitute multiline interpolations with relevant multiline substitutions so line information about errors should now be correctly displayed
57 |
58 | ### 1.1.1
59 | * FIX: Also catch errors regarding resolving absolute path of file and avoid them bubbling up to Stylelint
60 |
61 | ### 1.1.0
62 | * Add support for Node v4
63 | * Catch errors to avoid them bubbling up to Stylelint (which broke the Stylelint plugin for VSCode)
64 | * upgrade typescript-eslint-parser to 9.0.0
65 |
66 | ### 1.0.0
67 | * added support for interpolation tagging to take care of all interpolation edge cases
68 | * added moduleName option for better support for alternative libraries using a similar API
69 | * moved documentation to https://styled-components.com/docs/tooling#stylelint
70 |
71 | ### 0.4.0
72 | * move typescript to devDependencies
73 | * upgrade typescript-eslint-parser to 7.0.0
74 | * support .extend and .attrs Styled Components attributes
75 | * added shared stylelint config to configure a couple of rules to fit with styled-components’ style of writing CSS
76 |
77 | ### 0.3.0
78 | * fix interpolation following a comment edge case
79 | * rewrote our indentation handling fixing a lot of indentation rule problems
80 |
81 | ### 0.2.2
82 | * upgrade typescript-eslint-parser to 5.0.0
83 |
84 | ### 0.2.1
85 | * fix edge case in interpolations inside a declaration block
86 |
87 | ### 0.2.0
88 | * parse consecutive template literals for a single property
89 | * upgrade devDependencies
90 | * add interpolation linting section to README
91 | * make styled-mixin interpolation substitutes unique
92 | * added support for multiple interpolations in a property
93 | * handles interpolations in one-line css
94 | * support using stylelint-disable outside of tagged template literals
95 | * upgrade stylelint to 8.0.0
96 | * upgrade typescript-eslint-parser to 4.0.0
97 |
98 | ### 0.1.2
99 | * fix: move typescript from dependencies to devdependencies
100 |
101 | ### 0.1.1
102 | * add typescript support
103 | * add newline between components' styles
104 | * use unique name for each wrapped selector
105 | * fix: set stylelint result `errored` to `false` if `warning` does not contain errors
106 |
107 | ### 0.1.0
108 |
109 | * initial release
110 |
111 | ### 0.0.1 - 0.0.4
112 |
113 | * working draft
114 |
--------------------------------------------------------------------------------
/src/utils/general.js:
--------------------------------------------------------------------------------
1 | const CssError = require('postcss/lib/css-syntax-error')
2 | // selector count
3 | let count = 0
4 |
5 | /**
6 | * Based on https://github.com/mapbox/stylelint-processor-markdown
7 | * @author @davidtheclark
8 | * @author @sindresorhus
9 | */
10 | const fixIndentation = str => {
11 | // Get whitespaces
12 | const match = str.match(/^[ \t]*(?=\S|$)/gm)
13 |
14 | // Handle oneline TTL case and empty line etc.
15 | if (!match || match.length <= 1) {
16 | return {
17 | text: str,
18 | indentColumns: 0
19 | }
20 | }
21 |
22 | // We enforce that final backtick should be at base indentation level
23 | const baseIndentationLength = match[match.length - 1].length
24 | // Remove whitespace on empty lines before reindenting
25 | const emptyLinesHandled = str.replace(/^[ \t]$/gm, '')
26 | // Normalize indentation by removing common indent
27 | const re = new RegExp(String.raw`^[ \t]{${baseIndentationLength}}`, 'gm')
28 | const reIndentedString = emptyLinesHandled.replace(re, '')
29 | return {
30 | text: reIndentedString,
31 | indentColumns: baseIndentationLength
32 | }
33 | }
34 |
35 | const removeBaseIndentation = str => {
36 | // Remove empty lines
37 | const emptyLinesRemoved = str.replace(/^[ \t]*$/gm, '')
38 | // Get whitespaces
39 | const match = emptyLinesRemoved.match(/^[ \t]*(?=\S|$)/gm)
40 |
41 | if (!match || match.length <= 1) {
42 | return str.trim()
43 | }
44 |
45 | const indentationLevels = match.map(indent => indent.length).filter(indent => indent > 0)
46 | // Math,min would return infinity if list is empty
47 | if (indentationLevels.length === 0) {
48 | return str
49 | }
50 | const baseIndentationLevel = Math.min(...indentationLevels)
51 | // Remove baseIndentation from string
52 | const regex = new RegExp(String.raw`^[ \t]{${baseIndentationLevel}}`, 'gm')
53 | const baseIndentationRemoved = emptyLinesRemoved.replace(regex, '')
54 | return baseIndentationRemoved
55 | }
56 |
57 | const nextNonWhitespaceChar = text => {
58 | const matches = text.match(/^\s*([^\s])/)
59 | if (matches) {
60 | return matches[1]
61 | } else {
62 | return null
63 | }
64 | }
65 |
66 | const reverseString = str =>
67 | str
68 | .split('')
69 | .reverse()
70 | .join('')
71 |
72 | const isLastDeclarationCompleted = text => {
73 | // We disregard all comments in this assessment of declaration completion
74 | const commentsRemoved = text.replace(/\/\*[\s\S]*?\*\//g, '')
75 | const reversedText = reverseString(commentsRemoved)
76 | const lastNonWhitespaceChar = nextNonWhitespaceChar(reversedText)
77 | if (
78 | lastNonWhitespaceChar === ';' ||
79 | lastNonWhitespaceChar === '}' ||
80 | lastNonWhitespaceChar === '{' ||
81 | lastNonWhitespaceChar === null
82 | ) {
83 | return true
84 | } else {
85 | return false
86 | }
87 | }
88 |
89 | // eslint-disable-next-line no-return-assign
90 | const wrapSelector = content => `.selector${(count += 1)} {${content}}\n`
91 | const wrapKeyframes = content => `@keyframes {${content}}\n`
92 |
93 | /**
94 | * The reason we put a \s before .* in the last part of the regex is to make sure we don't
95 | * match stylelint-disable-line and stylelint-disable-next-line or, for now, any future extensions
96 | * as these line specific disables should not be placed outside a css TTL
97 | */
98 | const isStylelintComment = comment => /^\s*stylelint-(?:enable|disable)(?:\s.*)?$/.test(comment)
99 |
100 | const extrapolateShortenedCommand = (commands, shortCommand, absolutePath, location) => {
101 | let extrapolatedCommand = null
102 | // We use .some so we can break the loop using return true
103 | commands.some(singleCommand => {
104 | if (singleCommand.substr(0, shortCommand.length) === shortCommand) {
105 | if (extrapolatedCommand === null) {
106 | // This is the first time we found a match
107 | extrapolatedCommand = singleCommand
108 | } else {
109 | // We have already found another command which means this is not a unique short command.
110 | // This will probably never throw, as all our current commands start with different letters
111 | throw new CssError(
112 | 'You shortened a Styled Components interpolation tag ambiguously, add a few more characters to fix this error',
113 | location.line,
114 | location.column,
115 | undefined,
116 | absolutePath
117 | )
118 | }
119 | }
120 | // continue loop
121 | return false
122 | })
123 | return extrapolatedCommand
124 | }
125 |
126 | exports.wrapKeyframes = wrapKeyframes
127 | exports.wrapSelector = wrapSelector
128 | exports.fixIndentation = fixIndentation
129 | exports.reverseString = reverseString
130 | exports.nextNonWhitespaceChar = nextNonWhitespaceChar
131 | exports.isLastDeclarationCompleted = isLastDeclarationCompleted
132 | exports.isStylelintComment = isStylelintComment
133 | exports.extrapolateShortenedCommand = extrapolateShortenedCommand
134 | exports.removeBaseIndentation = removeBaseIndentation
135 |
--------------------------------------------------------------------------------
/test/hard.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 | const slash = require('slash')
4 |
5 | const processor = path.join(__dirname, '../src/index.js')
6 | const rules = {
7 | 'block-no-empty': true,
8 | indentation: 2,
9 | 'property-no-unknown': true
10 | }
11 |
12 | describe('hard', () => {
13 | let fixture
14 | let data
15 |
16 | // NOTE beforeEach() runs _after_ the beforeAll() hooks of the describe() blocks, so `fixture`
17 | // will have the right path
18 | beforeEach(done => {
19 | stylelint
20 | .lint({
21 | files: [fixture],
22 | syntax: 'scss',
23 | config: {
24 | processors: [processor],
25 | rules
26 | }
27 | })
28 | .then(result => {
29 | data = result
30 | done()
31 | })
32 | .catch(err => {
33 | data = err
34 | done()
35 | })
36 | })
37 |
38 | describe('extra indentation', () => {
39 | describe('valid', () => {
40 | beforeAll(() => {
41 | fixture = slash(path.join(__dirname, './fixtures/hard/indentation.js'))
42 | })
43 |
44 | it('should have one result', () => {
45 | expect(data.results.length).toEqual(1)
46 | })
47 |
48 | it('should use the right file', () => {
49 | expect(slash(data.results[0].source)).toEqual(fixture)
50 | })
51 |
52 | it('should not have errored', () => {
53 | expect(data.errored).toEqual(false)
54 | })
55 |
56 | it('should not have any warnings', () => {
57 | expect(data.results[0].warnings).toEqual([])
58 | })
59 | })
60 |
61 | describe('invalid', () => {
62 | beforeAll(() => {
63 | fixture = slash(path.join(__dirname, './fixtures/hard/invalid-indentation.js'))
64 | })
65 |
66 | it('should have one result', () => {
67 | expect(data.results.length).toEqual(1)
68 | })
69 |
70 | it('should use the right file', () => {
71 | expect(slash(data.results[0].source)).toEqual(fixture)
72 | })
73 |
74 | it('should have errored', () => {
75 | expect(data.errored).toEqual(true)
76 | })
77 |
78 | it('should have 13 warnings', () => {
79 | expect(data.results[0].warnings.length).toEqual(13)
80 | })
81 |
82 | it('should all be indentation warnings', () => {
83 | data.results[0].warnings.forEach(warning => {
84 | expect(warning.rule).toEqual('indentation')
85 | })
86 | })
87 | })
88 | })
89 |
90 | describe('source maps', () => {
91 | beforeAll(() => {
92 | fixture = slash(path.join(__dirname, './fixtures/hard/source-maps.js'))
93 | })
94 |
95 | it('should have one result', () => {
96 | expect(data.results.length).toEqual(1)
97 | })
98 |
99 | it('should use the right file', () => {
100 | expect(slash(data.results[0].source)).toEqual(fixture)
101 | })
102 |
103 | it('should have errored', () => {
104 | expect(data.errored).toEqual(true)
105 | })
106 |
107 | it('should have six warnings', () => {
108 | expect(data.results[0].warnings.length).toEqual(6)
109 | })
110 |
111 | it('should have five warnings about indentation', () => {
112 | expect(data.results[0].warnings[0].rule).toEqual('property-no-unknown')
113 | expect(data.results[0].warnings[1].rule).toEqual('indentation')
114 | expect(data.results[0].warnings[2].rule).toEqual('indentation')
115 | expect(data.results[0].warnings[3].rule).toEqual('indentation')
116 | expect(data.results[0].warnings[4].rule).toEqual('indentation')
117 | expect(data.results[0].warnings[5].rule).toEqual('indentation')
118 | })
119 |
120 | it('should have a warning in line 5', () => {
121 | expect(data.results[0].warnings[1].line).toEqual(5)
122 | })
123 |
124 | it('should have a warning in line 15', () => {
125 | expect(data.results[0].warnings[2].line).toEqual(15)
126 | })
127 |
128 | it('should have a warning in line 22', () => {
129 | expect(data.results[0].warnings[3].line).toEqual(22)
130 | })
131 |
132 | it('should have a warning in line 28', () => {
133 | expect(data.results[0].warnings[4].line).toEqual(28)
134 | })
135 |
136 | it('should have a warning in line 35', () => {
137 | expect(data.results[0].warnings[5].line).toEqual(35)
138 | })
139 |
140 | it('should have a warning in line 42, column 31', () => {
141 | expect(data.results[0].warnings[0].line).toEqual(42)
142 | expect(data.results[0].warnings[0].column).toEqual(31)
143 | })
144 | })
145 |
146 | describe('js style comments', () => {
147 | describe('valid', () => {
148 | beforeAll(() => {
149 | fixture = slash(path.join(__dirname, './fixtures/hard/valid-js-comments.js'))
150 | })
151 |
152 | it('should have one result', () => {
153 | expect(data.results.length).toEqual(1)
154 | })
155 |
156 | it('should use the right file', () => {
157 | expect(slash(data.results[0].source)).toEqual(fixture)
158 | })
159 |
160 | it('should not have errored', () => {
161 | expect(data.errored).toEqual(false)
162 | })
163 |
164 | it('should not have any warnings', () => {
165 | expect(data.results[0].warnings).toEqual([])
166 | })
167 | })
168 | })
169 | })
170 |
--------------------------------------------------------------------------------
/test/ignore-rule-comments.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 | const slash = require('slash')
4 |
5 | const processor = path.join(__dirname, '../src/index.js')
6 | const rules = {
7 | 'color-named': 'never'
8 | }
9 |
10 | describe('ignore rule comments', () => {
11 | let fixture
12 | let data
13 |
14 | beforeEach(done => {
15 | stylelint
16 | .lint({
17 | files: [fixture],
18 | config: {
19 | processors: [processor],
20 | rules
21 | }
22 | })
23 | .then(result => {
24 | data = result
25 | done()
26 | })
27 | .catch(err => {
28 | data = err
29 | done()
30 | })
31 | })
32 |
33 | describe('disable-whole-file', () => {
34 | beforeAll(() => {
35 | fixture = slash(path.join(__dirname, './fixtures/ignore-rule-comments/disable-whole-file.js'))
36 | })
37 |
38 | it('should have one result', () => {
39 | expect(data.results.length).toBe(1)
40 | })
41 |
42 | it('should use the right file', () => {
43 | expect(slash(data.results[0].source)).toBe(fixture)
44 | })
45 |
46 | it('should not have errored', () => {
47 | expect(data.errored).toBe(false)
48 | })
49 |
50 | it('should not have any warnings', () => {
51 | expect(data.results[0].warnings.length).toBe(0)
52 | })
53 | })
54 |
55 | describe('alternating-disable-enable', () => {
56 | beforeAll(() => {
57 | fixture = slash(
58 | path.join(__dirname, './fixtures/ignore-rule-comments/alternating-disable-enable.js')
59 | )
60 | })
61 |
62 | it('should have one result', () => {
63 | expect(data.results.length).toBe(1)
64 | })
65 |
66 | it('should use the right file', () => {
67 | expect(slash(data.results[0].source)).toBe(fixture)
68 | })
69 |
70 | it('should have exactly 2 warnings', () => {
71 | expect(data.results[0].warnings.length).toBe(2)
72 | })
73 |
74 | it('should error at exactly the correct places', () => {
75 | const { warnings } = data.results[0]
76 |
77 | expect(warnings[0].line).toBe(4)
78 | expect(warnings[0].rule).toBe('color-named')
79 | expect(warnings[0].severity).toBe('error')
80 |
81 | expect(warnings[1].line).toBe(14)
82 | expect(warnings[1].rule).toBe('color-named')
83 | expect(warnings[1].severity).toBe('error')
84 | })
85 | })
86 |
87 | describe('use-single-line-comments', () => {
88 | beforeAll(() => {
89 | fixture = slash(
90 | path.join(__dirname, './fixtures/ignore-rule-comments/use-single-line-comments.js')
91 | )
92 | })
93 |
94 | it('should have one result', () => {
95 | expect(data.results.length).toBe(1)
96 | })
97 |
98 | it('should use the right file', () => {
99 | expect(slash(data.results[0].source)).toBe(fixture)
100 | })
101 |
102 | it('should have exactly 2 warnings', () => {
103 | expect(data.results[0].warnings.length).toBe(2)
104 | })
105 |
106 | it('should error at exactly the correct places', () => {
107 | const { warnings } = data.results[0]
108 |
109 | expect(warnings[0].line).toBe(4)
110 | expect(warnings[0].rule).toBe('color-named')
111 | expect(warnings[0].severity).toBe('error')
112 |
113 | expect(warnings[1].line).toBe(14)
114 | expect(warnings[1].rule).toBe('color-named')
115 | expect(warnings[1].severity).toBe('error')
116 | })
117 | })
118 |
119 | describe('use-single-line-disables', () => {
120 | beforeAll(() => {
121 | fixture = slash(
122 | path.join(__dirname, './fixtures/ignore-rule-comments/use-single-line-disables.js')
123 | )
124 | })
125 |
126 | it('should have one result', () => {
127 | expect(data.results.length).toBe(1)
128 | })
129 |
130 | it('should use the right file', () => {
131 | expect(slash(data.results[0].source)).toBe(fixture)
132 | })
133 |
134 | it('should have exactly 2 warnings', () => {
135 | expect(data.results[0].warnings.length).toBe(2)
136 | })
137 |
138 | it('should error at exactly the correct places', () => {
139 | const { warnings } = data.results[0]
140 |
141 | expect(warnings[0].line).toBe(5)
142 | expect(warnings[0].rule).toBe('color-named')
143 | expect(warnings[0].severity).toBe('error')
144 |
145 | expect(warnings[1].line).toBe(17)
146 | expect(warnings[1].rule).toBe('color-named')
147 | expect(warnings[1].severity).toBe('error')
148 | })
149 |
150 | it('should match css exactly', () => {
151 | // This is mostly to check that the disable comments are inserted as expected
152 | const regex = new RegExp(
153 | '^\\.selector\\d+ {\\n' +
154 | ' color: red;\\n' +
155 | '}\\n' +
156 | '\\n' +
157 | '/\\* stylelint-disable \\*/\\n' +
158 | '\\.selector\\d+ {\\n' +
159 | ' color: red;\\n' +
160 | '}\\n' +
161 | '\\n' +
162 | '/\\* stylelint-enable \\*/\\n' +
163 | '\\.selector\\d+ {\\n' +
164 | ' color: red;\\n' +
165 | '}\\n' +
166 | '$'
167 | )
168 | // eslint-disable-next-line
169 | const cssOutput = data.results[0]._postcssResult.css
170 | expect(regex.test(cssOutput)).toBe(true)
171 | })
172 | })
173 |
174 | describe('mix-in-css-disables', () => {
175 | beforeAll(() => {
176 | fixture = slash(
177 | path.join(__dirname, './fixtures/ignore-rule-comments/mix-in-css-disables.js')
178 | )
179 | })
180 |
181 | it('should have one result', () => {
182 | expect(data.results.length).toBe(1)
183 | })
184 |
185 | it('should use the right file', () => {
186 | expect(slash(data.results[0].source)).toBe(fixture)
187 | })
188 |
189 | it('should not have errored', () => {
190 | expect(data.errored).toBe(false)
191 | })
192 |
193 | it('should not have any warnings', () => {
194 | expect(data.results[0].warnings.length).toBe(0)
195 | })
196 | })
197 | })
198 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `stylelint-processor-styled-components`
2 |
3 | Lint your [styled components](https://github.com/styled-components/styled-components) with [stylelint](http://stylelint.io/)!
4 |
5 | [![Build Status][build-badge]][build-url]
6 | [![Coverage Status][coverage-badge]][coverage-url]
7 | [](https://spectrum.chat/styled-components/stylelint-processor)
8 | [![Greenkeeper][greenkeeper-badge]][greenkeeper-url]
9 |
10 | 
11 |
12 | ## Setup
13 |
14 | You need:
15 |
16 | - `stylelint` (duh)
17 | - This processor, to extract styles from `styled-components`
18 | - The [`stylelint-config-styled-components`](https://github.com/styled-components/stylelint-config-styled-components) config to disable stylelint rules that clash with `styled-components`
19 | - Your favorite `stylelint` config! (for example [`stylelint-config-recommended`](https://github.com/stylelint/stylelint-config-recommended))
20 |
21 | ```
22 | (npm install --save-dev \
23 | stylelint \
24 | stylelint-processor-styled-components \
25 | stylelint-config-styled-components \
26 | stylelint-config-recommended)
27 | ```
28 |
29 | Now use those in your `.stylelintrc` and run stylelint with your JavaScript files!
30 |
31 | ```json
32 | {
33 | "processors": ["stylelint-processor-styled-components"],
34 | "extends": [
35 | "stylelint-config-recommended",
36 | "stylelint-config-styled-components"
37 | ]
38 | }
39 | ```
40 |
41 | > **NOTE:** The processor works with Flow- and TypeScript-typed files too! (we'll assume TypeScript usage if your files end in `.ts` or `.tsx`)
42 |
43 | And it also has some options. Their default values are,
44 |
45 | ```json
46 | {
47 | "processors": [["stylelint-processor-styled-components", {
48 | "moduleName": "styled-components",
49 | "importName": "default",
50 | "strict": false,
51 | "ignoreFiles": [],
52 | "parserPlugins": [
53 | "jsx",
54 | ["decorators", { "decoratorsBeforeExport": true }],
55 | "classProperties",
56 | "exportExtensions",
57 | "functionBind",
58 | "functionSent"
59 | ]
60 | }]]
61 | }
62 | ```
63 |
64 | - Combining with `moduleName`, `importName` and `strict`, you can tell the processor what kinds of tagged template literals to lint.
65 |
66 | ```
67 | import styled, { css, keyframes } from 'styled-components';
68 |
69 | // `importName` from `moduleName`, which means where `styled` comes from
70 | styled(Component)``;
71 | styled('div')``;
72 | styled.div``;
73 |
74 | // any other imports from `moduleName` (if `strict` is true, they will not be linted)
75 | css``;
76 | keyframes``;
77 |
78 | // special extend calls, which have been deprecated in styled-components v4
79 | Component.extend``;
80 |
81 | ```
82 |
83 | - `ignoreFiles` is passed to [micromatch](https://github.com/micromatch/micromatch#api) as the second parameter, which means one or more glob patterns for matching.
84 |
85 | - `parserPlugins` is used to make the processor's parser be able to parse new syntaxes. All available babel parser plugins and related options can be found in [Babel's website](https://babeljs.io/docs/en/babel-parser#plugins).
86 |
87 | ## [Documentation](https://www.styled-components.com/docs/tooling#stylelint)
88 |
89 | **Further documentation for this processor lives on [the styled-components website](https://www.styled-components.com/docs/tooling#stylelint)!**
90 |
91 | - [Setup](https://www.styled-components.com/docs/tooling#setup)
92 | - [Webpack](https://www.styled-components.com/docs/tooling#webpack)
93 | - [Interpolation tagging](https://www.styled-components.com/docs/tooling#interpolation-tagging)
94 | - [Tags](https://www.styled-components.com/docs/tooling#tags)
95 | - [sc-custom](https://www.styled-components.com/docs/tooling#sc-custom)
96 | - [Syntax Notes](https://www.styled-components.com/docs/tooling#syntax-notes)
97 |
98 | ## F.A.Q.
99 |
100 | ### Why does it throw `Unexpected token`? Even thought the file didn't import `styled-components`.
101 |
102 | You can custom babel plugins by `option.parserPlugins` now. An API example is [our test](https://github.com/styled-components/stylelint-processor-styled-components/blob/master/test/options.test.js#L211). But if someone can implement [#231](https://github.com/styled-components/stylelint-processor-styled-components/issues/231), that will be much better.
103 |
104 | If your project includes `yarn.lock` or `package-lock.json`, an alternative cause can be that babel related dependencies, i.e. `@babel/parser` and `@babel/traverse`, are outdated, especially when linting files with new TypeScript syntaxes. You can upgrade them by removing their entries in the lockfile and reinstall dependencies.
105 |
106 | ### Why does it throw unexpected lint errors?
107 |
108 | The processor can not always parse interpolations with right things. But you can use [interpolation-tagging](https://www.styled-components.com/docs/tooling#interpolation-tagging) to help it. If you have ideas to make it more intelligent, feel free to send a PR or share your solution by an new issue.
109 |
110 | What's more, if set `syntax: css-in-js` in stylelint@10, it can extract styles from `styled-components` without this processor. Even though there are still lots of differences with this processor, we hope this processor's abilities can be migrated to stylelint totally in the future.
111 |
112 | ### I don't want specified tagged template literal to be parsed, i.e. `css`.
113 |
114 | You can set `option.strict`. More examples are in [#258](https://github.com/styled-components/stylelint-processor-styled-components/pull/258).
115 |
116 | ## License
117 |
118 | Licensed under the MIT License, Copyright © 2017 Maximilian Stoiber. See [LICENSE.md](./LICENSE.md) for more information!
119 |
120 | Based on Mapbox' excellent [`stylelint-processor-markdown`](https://github.com/mapbox/stylelint-processor-markdown), thanks to @davidtheclark!
121 |
122 | [build-badge]: https://travis-ci.org/styled-components/stylelint-processor-styled-components.svg?branch=master
123 | [build-url]: https://travis-ci.org/styled-components/stylelint-processor-styled-components
124 | [coverage-badge]: https://coveralls.io/repos/github/styled-components/stylelint-processor-styled-components/badge.svg?branch=master
125 | [coverage-url]: https://coveralls.io/github/styled-components/stylelint-processor-styled-components?branch=master
126 | [greenkeeper-badge]: https://badges.greenkeeper.io/styled-components/stylelint-processor-styled-components.svg
127 | [greenkeeper-url]: https://greenkeeper.io/
128 |
--------------------------------------------------------------------------------
/src/utils/tagged-template-literal.js:
--------------------------------------------------------------------------------
1 | const CssError = require('postcss/lib/css-syntax-error')
2 | const {
3 | reverseString,
4 | nextNonWhitespaceChar,
5 | isLastDeclarationCompleted,
6 | extrapolateShortenedCommand
7 | } = require('./general')
8 |
9 | /**
10 | * Check if a node is a tagged template literal
11 | */
12 | const isTaggedTemplateLiteral = node => node.type === 'TaggedTemplateExpression'
13 |
14 | /**
15 | * Check if a tagged template literal has interpolations
16 | */
17 | const hasInterpolations = node => !node.quasi.quasis[0].tail
18 |
19 | /**
20 | * Retrieves all the starting and ending comments of a TTL expression
21 | */
22 | const retrieveStartEndComments = expression =>
23 | (expression.leadingComments || []).concat(expression.trailingComments || [])
24 |
25 | /**
26 | * Checks if given comment value is an interpolation tag
27 | */
28 | const isScTag = comment => /^\s*?sc-[a-z]/.test(comment)
29 |
30 | /**
31 | * Checks if an interpolation has an sc comment tag
32 | */
33 | const hasInterpolationTag = expression => {
34 | const relevantComments = retrieveStartEndComments(expression).map(
35 | commentObject => commentObject.value
36 | )
37 | return relevantComments.some(isScTag)
38 | }
39 |
40 | const extractScTagInformation = comment => {
41 | const matchArray = comment.match(/^\s*?sc-([a-z]+)\s*(?:(?:'(.*?)')|(?:"(.*?)"))?\s*$/)
42 | if (matchArray === null) {
43 | return null
44 | }
45 | return {
46 | command: matchArray[1],
47 | // This is only cared about if command is custom
48 | customPlaceholder: matchArray[2] || matchArray[3]
49 | }
50 | }
51 |
52 | const interpolationTagAPI = ['block', 'selector', 'declaration', 'property', 'value', 'custom']
53 | /**
54 | * Enact the interpolation tagging API
55 | */
56 | const parseInterpolationTag = (expression, id, absolutePath) => {
57 | const relevantComments = retrieveStartEndComments(expression)
58 | let substitute
59 | relevantComments.some(comment => {
60 | if (isScTag(comment.value)) {
61 | // We always assume that there is only one sc tag in an interpolation
62 | const scTagInformation = extractScTagInformation(comment.value)
63 | if (scTagInformation === null) {
64 | throw new CssError(
65 | 'We were unable to parse your Styled Components interpolation tag, this is most likely due to lack of quotes in an sc-custom tag, refer to the documentation for correct format',
66 | comment.loc.start.line,
67 | comment.loc.start.column,
68 | undefined,
69 | absolutePath
70 | )
71 | }
72 | scTagInformation.command = extrapolateShortenedCommand(
73 | interpolationTagAPI,
74 | scTagInformation.command,
75 | absolutePath,
76 | comment.loc.start
77 | )
78 | switch (scTagInformation.command) {
79 | case 'selector':
80 | substitute = `.sc-selector${id}`
81 | break
82 |
83 | case 'block':
84 | case 'declaration':
85 | substitute = `-styled-mixin${id}: dummyValue;`
86 | break
87 |
88 | case 'property':
89 | substitute = `-styled-mixin${id}`
90 | break
91 |
92 | case 'value':
93 | substitute = '$dummyValue'
94 | break
95 |
96 | case 'custom':
97 | substitute = scTagInformation.customPlaceholder
98 | break
99 |
100 | default:
101 | throw new CssError(
102 | 'You tagged a Styled Components interpolation with an invalid sc- tag. Refer to the documentation to see valid interpolation tags',
103 | comment.loc.start.line,
104 | comment.loc.start.column,
105 | undefined,
106 | absolutePath
107 | )
108 | }
109 | return true // Break loop
110 | }
111 | return false // Continue loop
112 | })
113 | return substitute
114 | }
115 |
116 | /**
117 | * Merges the interpolations in a parsed tagged template literals with the strings
118 | */
119 | const interleave = (quasis, expressions, absolutePath) => {
120 | // Used for making sure our dummy mixins are all unique
121 | let count = 0
122 | let css = ''
123 | for (let i = 0, l = expressions.length; i < l; i += 1) {
124 | const prevText = quasis[i].value.raw
125 | const nextText = quasis[i + 1].value.raw
126 | const prevChar = nextNonWhitespaceChar(reverseString(prevText))
127 | const nextChar = nextNonWhitespaceChar(nextText)
128 |
129 | css += prevText
130 | let substitute
131 | if (hasInterpolationTag(expressions[i])) {
132 | substitute = parseInterpolationTag(expressions[i], count, absolutePath)
133 | count += 1
134 | // No sc tag so we guess defaults
135 | } else if (nextChar === '{') {
136 | // Guess as selector, which shares format with `parseInterpolationTag`, but not `wrapSelector`
137 | substitute = `.sc-selector${count}`
138 | count += 1
139 | } else if (prevChar === ':') {
140 | // After a colon and not a pseudo-class, then guess as value
141 | substitute = '$dummyValue'
142 | } else if (nextChar === ':') {
143 | // Before a colon, then guess as property
144 | substitute = `-styled-mixin${count}`
145 | count += 1
146 | } else if (isLastDeclarationCompleted(css)) {
147 | /** This block assumes that if you put an interpolation in the position
148 | * of the start of a declaration that the interpolation will
149 | * contain a full declaration and not later in the template literal
150 | * be completed by another interpolation / completed by following text
151 | * in the literal
152 | */
153 | substitute = `-styled-mixin${count}: dummyValue`
154 | count += 1
155 | if (nextNonWhitespaceChar(nextText) !== ';') {
156 | substitute += ';'
157 | }
158 | } else {
159 | /* This block assumes that we are in the middle of a declaration
160 | * and that the interpolation is providing a value, not a property
161 | * or part of a property
162 | */
163 | substitute = '$dummyValue'
164 | }
165 | // Make sure substituted by same count of lines
166 | const targetLines = quasis[i + 1].loc.start.line - quasis[i].loc.end.line + 1
167 | let currentLines = substitute.split('\n').length
168 | while (currentLines < targetLines) {
169 | substitute += '\n/* dummyComment */'
170 | currentLines += 1
171 | }
172 |
173 | css += substitute
174 | }
175 | css += quasis[quasis.length - 1].value.raw
176 | return css
177 | }
178 |
179 | /**
180 | * Get the content of a tagged template literal
181 | *
182 | * TODO Cover edge cases
183 | */
184 | const getTaggedTemplateLiteralContent = (node, absolutePath) => {
185 | if (hasInterpolations(node)) {
186 | return interleave(node.quasi.quasis, node.quasi.expressions, absolutePath)
187 | } else {
188 | return node.quasi.quasis[0].value.raw
189 | }
190 | }
191 |
192 | exports.isTaggedTemplateLiteral = isTaggedTemplateLiteral
193 | exports.getTaggedTemplateLiteralContent = getTaggedTemplateLiteralContent
194 | exports.interleave = interleave
195 | exports.hasInterpolationTag = hasInterpolationTag
196 | exports.parseInterpolationTag = parseInterpolationTag
197 | exports.extractScTagInformation = extractScTagInformation
198 |
--------------------------------------------------------------------------------
/test/simple.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 | const slash = require('slash')
4 |
5 | const processor = path.join(__dirname, '../src/index.js')
6 | const rules = {
7 | 'block-no-empty': true,
8 | indentation: 2,
9 | 'rule-empty-line-before': [
10 | 'always-multi-line',
11 | {
12 | except: ['first-nested'],
13 | ignore: ['after-comment']
14 | }
15 | ],
16 | 'selector-type-no-unknown': true
17 | }
18 |
19 | describe('simple', () => {
20 | let fixture
21 | let data
22 |
23 | // NOTE beforeEach() runs _after_ the beforeAll() hooks of the describe() blocks, so `fixture`
24 | // will have the right path
25 | beforeEach(done => {
26 | stylelint
27 | .lint({
28 | files: [fixture],
29 | config: {
30 | processors: [processor],
31 | rules
32 | }
33 | })
34 | .then(result => {
35 | data = result
36 | done()
37 | })
38 | .catch(err => {
39 | // eslint-disable-next-line
40 | console.log(err)
41 | data = err
42 | done()
43 | })
44 | })
45 |
46 | describe('valid fixtures', () => {
47 | beforeAll(() => {
48 | fixture = slash(path.join(__dirname, './fixtures/simple/valid.js'))
49 | })
50 |
51 | it('should have one result', () => {
52 | expect(data.results.length).toEqual(1)
53 | })
54 |
55 | it('should use the right file', () => {
56 | expect(slash(data.results[0].source)).toEqual(fixture)
57 | })
58 |
59 | it('should not have errored', () => {
60 | expect(data.errored).toEqual(false)
61 | })
62 |
63 | it('should not have any warnings', () => {
64 | expect(data.results[0].warnings.length).toEqual(0)
65 | })
66 | })
67 |
68 | describe('invalid fixtures', () => {
69 | beforeAll(() => {
70 | fixture = slash(path.join(__dirname, './fixtures/simple/invalid.js'))
71 | })
72 |
73 | it('should have one result', () => {
74 | expect(data.results.length).toEqual(1)
75 | })
76 |
77 | it('should use the right file', () => {
78 | expect(slash(data.results[0].source)).toEqual(fixture)
79 | })
80 |
81 | it('should have errored', () => {
82 | expect(data.results[0].errored).toEqual(true)
83 | })
84 |
85 | it('should have two warnings (i.e. wrong lines of code)', () => {
86 | expect(data.results[0].warnings.length).toEqual(2)
87 | })
88 |
89 | it('should have a block-no-empty as the first warning', () => {
90 | expect(data.results[0].warnings[0].rule).toEqual('block-no-empty')
91 | })
92 |
93 | it('should have an indentation as the first warning', () => {
94 | expect(data.results[0].warnings[1].rule).toEqual('indentation')
95 | })
96 | })
97 |
98 | describe('helpers', () => {
99 | beforeAll(() => {
100 | fixture = slash(path.join(__dirname, './fixtures/simple/helpers.js'))
101 | })
102 |
103 | it('should have one result', () => {
104 | expect(data.results.length).toEqual(1)
105 | })
106 |
107 | it('should use the right file', () => {
108 | expect(slash(data.results[0].source)).toEqual(fixture)
109 | })
110 |
111 | it('should have errored', () => {
112 | expect(data.results[0].errored).toEqual(true)
113 | })
114 |
115 | it('should have 11 warnings (i.e. wrong lines of code)', () => {
116 | expect(data.results[0].warnings.length).toEqual(11)
117 | })
118 |
119 | it('should be indentation and "empty line before" warnings', () => {
120 | const warnings = data.results[0].warnings.reduce(
121 | (all, { rule }) => (all.indexOf(rule) >= 0 ? all : all.concat(rule)),
122 | []
123 | )
124 |
125 | expect(warnings).toEqual(['rule-empty-line-before', 'indentation'])
126 | })
127 | })
128 |
129 | describe('import names', () => {
130 | beforeAll(() => {
131 | fixture = slash(path.join(__dirname, './fixtures/simple/imports.js'))
132 | })
133 |
134 | it('should have one result', () => {
135 | expect(data.results.length).toEqual(1)
136 | })
137 |
138 | it('should use the right file', () => {
139 | expect(slash(data.results[0].source)).toEqual(fixture)
140 | })
141 |
142 | it('should have errored, even with a different name', () => {
143 | expect(data.results[0].errored).toEqual(true)
144 | })
145 |
146 | it('should have 9 warnings, even with a different name (i.e. wrong lines of code)', () => {
147 | expect(data.results[0].warnings.length).toEqual(9)
148 | })
149 |
150 | it('should all be indentation warnings, even with a different name', () => {
151 | data.results[0].warnings.forEach(warning => {
152 | expect(warning.rule).toEqual('indentation')
153 | })
154 | })
155 | })
156 |
157 | describe('nesting', () => {
158 | beforeAll(() => {
159 | fixture = slash(path.join(__dirname, './fixtures/simple/nesting.js'))
160 | })
161 |
162 | it('should have one result', () => {
163 | expect(data.results.length).toEqual(1)
164 | })
165 |
166 | it('should use the right file', () => {
167 | expect(slash(data.results[0].source)).toEqual(fixture)
168 | })
169 |
170 | it('should not have errored', () => {
171 | expect(data.errored).toEqual(false)
172 | })
173 |
174 | it('should not have any warnings', () => {
175 | expect(data.results[0].warnings.length).toEqual(0)
176 | })
177 | })
178 |
179 | describe('global variables', () => {
180 | beforeAll(() => {
181 | fixture = slash(path.join(__dirname, './fixtures/simple/global.js'))
182 | })
183 |
184 | it('should have one result', () => {
185 | expect(data.results.length).toEqual(1)
186 | })
187 |
188 | it('should use the right file', () => {
189 | expect(slash(data.results[0].source)).toEqual(fixture)
190 | })
191 |
192 | it('should have errored', () => {
193 | expect(data.errored).toEqual(true)
194 | })
195 |
196 | it('should have 5 warnings', () => {
197 | expect(data.results[0].warnings.length).toEqual(5)
198 | })
199 | })
200 |
201 | describe('other library', () => {
202 | beforeAll(() => {
203 | fixture = slash(path.join(__dirname, './fixtures/simple/other-library.js'))
204 | })
205 |
206 | it('should have one result', () => {
207 | expect(data.results.length).toEqual(1)
208 | })
209 |
210 | it('should use the right file', () => {
211 | expect(slash(data.results[0].source)).toEqual(fixture)
212 | })
213 |
214 | it('should have errored', () => {
215 | expect(data.errored).toEqual(true)
216 | })
217 |
218 | it('should have 1 warning', () => {
219 | expect(data.results[0].warnings.length).toEqual(1)
220 | })
221 | })
222 |
223 | describe('identify styled', () => {
224 | beforeAll(() => {
225 | fixture = slash(path.join(__dirname, './fixtures/simple/identify-styled.js'))
226 | })
227 |
228 | it('should have one result', () => {
229 | expect(data.results.length).toEqual(1)
230 | })
231 |
232 | it('should use the right file', () => {
233 | expect(slash(data.results[0].source)).toEqual(fixture)
234 | })
235 |
236 | it('should have errored', () => {
237 | expect(data.errored).toEqual(true)
238 | })
239 |
240 | it('should have 5 warnings', () => {
241 | expect(data.results[0].warnings.length).toEqual(5)
242 | })
243 |
244 | it('should have correct warnings', () => {
245 | const { warnings } = data.results[0]
246 | expect(warnings[0].line).toBe(4)
247 | expect(warnings[0].rule).toBe('selector-type-no-unknown')
248 |
249 | expect(warnings[1].line).toBe(10)
250 | expect(warnings[1].rule).toBe('selector-type-no-unknown')
251 |
252 | expect(warnings[2].line).toBe(16)
253 | expect(warnings[2].rule).toBe('selector-type-no-unknown')
254 |
255 | expect(warnings[3].line).toBe(22)
256 | expect(warnings[3].rule).toBe('selector-type-no-unknown')
257 |
258 | expect(warnings[4].line).toBe(28)
259 | expect(warnings[4].rule).toBe('selector-type-no-unknown')
260 | })
261 | })
262 |
263 | describe('parse successfully', () => {
264 | beforeAll(() => {
265 | fixture = slash(path.join(__dirname, './fixtures/simple/parse.js'))
266 | })
267 |
268 | it('should have one result', () => {
269 | expect(data.results.length).toEqual(1)
270 | })
271 |
272 | it('should use the right file', () => {
273 | expect(slash(data.results[0].source)).toEqual(fixture)
274 | })
275 |
276 | it('should not have errored', () => {
277 | expect(data.errored).toEqual(false)
278 | })
279 |
280 | it('should not have any warnings', () => {
281 | expect(data.results[0].warnings.length).toEqual(0)
282 | })
283 | })
284 | })
285 |
--------------------------------------------------------------------------------
/test/options.test.js:
--------------------------------------------------------------------------------
1 | const stylelint = require('stylelint')
2 | const path = require('path')
3 | const slash = require('slash')
4 |
5 | const processor = path.join(__dirname, '../src/index.js')
6 | const rules = {
7 | 'block-no-empty': true,
8 | indentation: 2,
9 | 'rule-empty-line-before': [
10 | 'always-multi-line',
11 | {
12 | except: ['first-nested'],
13 | ignore: ['after-comment']
14 | }
15 | ],
16 | 'selector-type-no-unknown': true
17 | }
18 |
19 | describe('options', () => {
20 | let fixture
21 | let data
22 |
23 | describe('moduleName', () => {
24 | // NOTE beforeEach() runs _after_ the beforeAll() hooks of the describe() blocks, so `fixture`
25 | // will have the right path
26 | beforeEach(done => {
27 | stylelint
28 | .lint({
29 | files: [fixture],
30 | config: {
31 | // Set moduleName option to "emotion"
32 | processors: [[processor, { moduleName: 'emotion' }]],
33 | rules
34 | }
35 | })
36 | .then(result => {
37 | data = result
38 | done()
39 | })
40 | .catch(err => {
41 | // eslint-disable-next-line no-console
42 | console.log(err)
43 | data = err
44 | done()
45 | })
46 | })
47 |
48 | describe('moduleName', () => {
49 | beforeAll(() => {
50 | fixture = slash(path.join(__dirname, './fixtures/options/module-name.js'))
51 | })
52 |
53 | it('should have one result', () => {
54 | expect(data.results.length).toEqual(1)
55 | })
56 |
57 | it('should use the right file', () => {
58 | expect(slash(data.results[0].source)).toEqual(fixture)
59 | })
60 |
61 | it('should have errored', () => {
62 | expect(data.results[0].errored).toEqual(true)
63 | })
64 |
65 | it('should have one warning (i.e. wrong lines of code)', () => {
66 | expect(data.results[0].warnings.length).toEqual(1)
67 | })
68 |
69 | it('should have a block-no-empty as the first warning', () => {
70 | expect(data.results[0].warnings[0].rule).toEqual('block-no-empty')
71 | })
72 | })
73 |
74 | describe('relative moduleName', () => {
75 | beforeAll(() => {
76 | fixture = slash(path.join(__dirname, './fixtures/options/relative-module-name.js'))
77 | })
78 |
79 | it('should have one result', () => {
80 | expect(data.results.length).toEqual(1)
81 | })
82 |
83 | it('should use the right file', () => {
84 | expect(slash(data.results[0].source)).toEqual(fixture)
85 | })
86 |
87 | it('should have errored', () => {
88 | expect(data.results[0].errored).toEqual(true)
89 | })
90 |
91 | it('should have one warning (i.e. wrong lines of code)', () => {
92 | expect(data.results[0].warnings.length).toEqual(1)
93 | })
94 |
95 | it('should have a block-no-empty as the first warning', () => {
96 | expect(data.results[0].warnings[0].rule).toEqual('block-no-empty')
97 | })
98 | })
99 |
100 | describe('invalid moduleName', () => {
101 | beforeAll(() => {
102 | fixture = slash(path.join(__dirname, './fixtures/options/invalid-module-name.js'))
103 | })
104 |
105 | it('should have one result', () => {
106 | expect(data.results.length).toEqual(1)
107 | })
108 |
109 | it('should use the right file', () => {
110 | expect(slash(data.results[0].source)).toEqual(fixture)
111 | })
112 |
113 | it('should not have errored', () => {
114 | expect(data.results[0].errored).toEqual(false)
115 | })
116 | })
117 | })
118 |
119 | describe('importName', () => {
120 | // NOTE beforeEach() runs _after_ the beforeAll() hooks of the describe() blocks, so `fixture`
121 | // will have the right path
122 | beforeEach(done => {
123 | stylelint
124 | .lint({
125 | files: [fixture],
126 | config: {
127 | // Set importName option to "notDefault"
128 | processors: [[processor, { importName: 'notDefault' }]],
129 | rules
130 | }
131 | })
132 | .then(result => {
133 | data = result
134 | done()
135 | })
136 | .catch(err => {
137 | // eslint-disable-next-line no-console
138 | console.log(err)
139 | data = err
140 | done()
141 | })
142 | })
143 |
144 | describe('importName', () => {
145 | beforeAll(() => {
146 | fixture = slash(path.join(__dirname, './fixtures/options/import-name.js'))
147 | })
148 |
149 | it('should have one result', () => {
150 | expect(data.results.length).toEqual(1)
151 | })
152 |
153 | it('should use the right file', () => {
154 | expect(slash(data.results[0].source)).toEqual(fixture)
155 | })
156 |
157 | it('should have errored', () => {
158 | expect(data.results[0].errored).toEqual(true)
159 | })
160 |
161 | it('should have one warning (i.e. wrong lines of code)', () => {
162 | expect(data.results[0].warnings.length).toEqual(1)
163 | })
164 |
165 | it('should have a block-no-empty as the first warning', () => {
166 | expect(data.results[0].warnings[0].rule).toEqual('block-no-empty')
167 | })
168 | })
169 |
170 | describe('invalid importName', () => {
171 | beforeAll(() => {
172 | fixture = slash(path.join(__dirname, './fixtures/options/invalid-import-name.js'))
173 | })
174 |
175 | it('should have one result', () => {
176 | expect(data.results.length).toEqual(1)
177 | })
178 |
179 | it('should use the right file', () => {
180 | expect(slash(data.results[0].source)).toEqual(fixture)
181 | })
182 |
183 | it('should not have errored', () => {
184 | expect(data.results[0].errored).toEqual(false)
185 | })
186 | })
187 | })
188 |
189 | describe('parserPlugins', () => {
190 | // NOTE beforeEach() runs _after_ the beforeAll() hooks of the describe() blocks, so `fixture`
191 | // will have the right path
192 | beforeEach(done => {
193 | const plugins = [
194 | 'jsx',
195 | ['decorators', { decoratorsBeforeExport: true }],
196 | 'classProperties',
197 | 'exportExtensions',
198 | 'functionBind',
199 | 'functionSent',
200 | // Enable experimental feature
201 | 'exportDefaultFrom'
202 | ]
203 |
204 | stylelint
205 | .lint({
206 | code: "export Container from './Container';",
207 | config: {
208 | processors: [[processor, { parserPlugins: plugins }]],
209 | rules
210 | }
211 | })
212 | .then(result => {
213 | data = result
214 | done()
215 | })
216 | .catch(err => {
217 | // eslint-disable-next-line no-console
218 | console.log(err)
219 | data = err
220 | done()
221 | })
222 | })
223 |
224 | it('should have one result', () => {
225 | expect(data.results.length).toEqual(1)
226 | })
227 |
228 | it('should have not errored', () => {
229 | expect(data.results[0].errored).toEqual(false)
230 | })
231 | })
232 |
233 | describe('strict', () => {
234 | beforeEach(done => {
235 | fixture = slash(path.join(__dirname, './fixtures/options/strict.js'))
236 |
237 | stylelint
238 | .lint({
239 | files: [fixture],
240 | config: {
241 | processors: [
242 | [
243 | processor,
244 | {
245 | moduleName: 'some-module',
246 | importName: 'foo',
247 | strict: true
248 | }
249 | ]
250 | ],
251 | rules
252 | }
253 | })
254 | .then(result => {
255 | data = result
256 | done()
257 | })
258 | .catch(err => {
259 | // eslint-disable-next-line no-console
260 | console.log(err)
261 | data = err
262 | done()
263 | })
264 | })
265 |
266 | it('should have one result', () => {
267 | expect(data.results.length).toEqual(1)
268 | })
269 |
270 | it('should use the right file', () => {
271 | expect(slash(data.results[0].source)).toEqual(fixture)
272 | })
273 |
274 | it('should have errored', () => {
275 | expect(data.results[0].errored).toEqual(true)
276 | })
277 |
278 | it('should have one warning (i.e. wrong lines of code)', () => {
279 | expect(data.results[0].warnings.length).toEqual(1)
280 | })
281 |
282 | it('should have a block-no-empty as the first warning', () => {
283 | expect(data.results[0].warnings[0].rule).toEqual('block-no-empty')
284 | })
285 | })
286 | })
287 |
--------------------------------------------------------------------------------
/test/utils.test.js:
--------------------------------------------------------------------------------
1 | const {
2 | interleave,
3 | hasInterpolationTag,
4 | parseInterpolationTag,
5 | extractScTagInformation
6 | } = require('../src/utils/tagged-template-literal')
7 | const {
8 | isLastDeclarationCompleted,
9 | nextNonWhitespaceChar,
10 | reverseString,
11 | isStylelintComment,
12 | fixIndentation,
13 | extrapolateShortenedCommand,
14 | removeBaseIndentation
15 | } = require('../src/utils/general')
16 | const { isCausedBySubstitution, getCorrectColumn } = require('../src/utils/result')
17 |
18 | const mockLoc = (startLine, endLine) => ({
19 | start: {
20 | line: startLine
21 | },
22 | end: {
23 | line: endLine
24 | }
25 | })
26 |
27 | describe('utils', () => {
28 | describe('interleave', () => {
29 | it('should return the value of the node if no interpolation exists', () => {
30 | const raw = 'color: blue;'
31 | const quasis = [
32 | {
33 | value: {
34 | raw
35 | }
36 | }
37 | ]
38 | expect(interleave(quasis, [])).toEqual(raw)
39 | })
40 |
41 | it('should variabelize an interpolation', () => {
42 | const quasis = [
43 | {
44 | loc: mockLoc(1, 3),
45 | value: {
46 | raw: '\n display: block;\n color: '
47 | }
48 | },
49 | {
50 | loc: mockLoc(3, 5),
51 | value: {
52 | raw: ';\n background: blue;\n'
53 | }
54 | }
55 | ]
56 | const expressions = [
57 | {
58 | loc: mockLoc(3, 3),
59 | name: 'color'
60 | }
61 | ]
62 | expect(interleave(quasis, expressions)).toEqual(
63 | '\n display: block;\n color: $dummyValue;\n background: blue;\n'
64 | )
65 | })
66 |
67 | it('converts interpolated expressions to dummy mixins', () => {
68 | const quasis = [
69 | {
70 | loc: mockLoc(1, 3),
71 | value: {
72 | raw: '\n display: block;\n '
73 | }
74 | },
75 | {
76 | loc: mockLoc(3, 5),
77 | value: {
78 | raw: '\n background: blue;\n'
79 | }
80 | }
81 | ]
82 | const expressions = [
83 | {
84 | loc: mockLoc(3, 3),
85 | name: undefined
86 | }
87 | ]
88 | expect(interleave(quasis, expressions)).toEqual(
89 | '\n display: block;\n -styled-mixin0: dummyValue;\n background: blue;\n'
90 | )
91 | })
92 |
93 | it('correctly converts several interpolations within a single property', () => {
94 | const quasis = [
95 | {
96 | loc: mockLoc(1, 3),
97 | value: {
98 | raw: '\n display: block;\n border: '
99 | }
100 | },
101 | {
102 | loc: mockLoc(3, 3),
103 | value: {
104 | raw: ' '
105 | }
106 | },
107 | {
108 | loc: mockLoc(3, 3),
109 | value: {
110 | raw: ' '
111 | }
112 | },
113 | {
114 | loc: mockLoc(3, 5),
115 | value: {
116 | raw: ';\n background: blue;\n'
117 | }
118 | }
119 | ]
120 | const expressions = [
121 | {
122 | loc: mockLoc(3, 3),
123 | name: 'borderWidth'
124 | },
125 | {
126 | loc: mockLoc(3, 3),
127 | name: 'borderStyle'
128 | },
129 | {
130 | loc: mockLoc(3, 3),
131 | name: 'color'
132 | }
133 | ]
134 | expect(interleave(quasis, expressions)).toEqual(
135 | '\n display: block;\n border: $dummyValue $dummyValue $dummyValue;\n background: blue;\n'
136 | )
137 | })
138 |
139 | it('correctly handles several interpolations in single line of css', () => {
140 | const quasis1 = [
141 | {
142 | loc: mockLoc(1, 2),
143 | value: {
144 | raw: '\n display: '
145 | }
146 | },
147 | {
148 | loc: mockLoc(2, 2),
149 | value: {
150 | raw: '; background: '
151 | }
152 | },
153 | {
154 | loc: mockLoc(2, 3),
155 | value: {
156 | raw: ';\n'
157 | }
158 | }
159 | ]
160 | const expressions1 = [
161 | {
162 | loc: mockLoc(2, 2),
163 | name: 'display'
164 | },
165 | {
166 | loc: mockLoc(2, 2),
167 | name: 'background'
168 | }
169 | ]
170 | expect(interleave(quasis1, expressions1)).toEqual(
171 | '\n display: $dummyValue; background: $dummyValue;\n'
172 | )
173 |
174 | const quasis2 = [
175 | {
176 | loc: mockLoc(1, 2),
177 | value: {
178 | raw: '\n display: '
179 | }
180 | },
181 | {
182 | loc: mockLoc(2, 2),
183 | value: {
184 | raw: '; '
185 | }
186 | },
187 | {
188 | loc: mockLoc(2, 3),
189 | value: {
190 | raw: '\n'
191 | }
192 | }
193 | ]
194 | const expressions2 = [
195 | {
196 | loc: mockLoc(2, 2),
197 | name: 'display'
198 | },
199 | {
200 | loc: mockLoc(3, 3),
201 | name: undefined
202 | }
203 | ]
204 | expect(interleave(quasis2, expressions2)).toEqual(
205 | '\n display: $dummyValue; -styled-mixin0: dummyValue;\n'
206 | )
207 |
208 | /**
209 | * It is important to also have this one as interleave would fail this if it simply
210 | * checked the previous quasi and not the previous processed text.
211 | * Here we also check the whole expression with and without a semi-colon in the quasi
212 | */
213 | const quasis3 = [
214 | {
215 | loc: mockLoc(1, 2),
216 | value: {
217 | raw: '\n display: '
218 | }
219 | },
220 | {
221 | loc: mockLoc(2, 2),
222 | value: {
223 | raw: '; '
224 | }
225 | },
226 | {
227 | loc: mockLoc(2, 2),
228 | value: {
229 | raw: ' '
230 | }
231 | },
232 | {
233 | loc: mockLoc(2, 3),
234 | value: {
235 | raw: '\n'
236 | }
237 | }
238 | ]
239 | const expressions3 = [
240 | {
241 | loc: mockLoc(2, 2),
242 | name: 'display'
243 | },
244 | {
245 | loc: mockLoc(2, 2),
246 | name: undefined
247 | },
248 | {
249 | loc: mockLoc(2, 2),
250 | name: undefined
251 | }
252 | ]
253 | expect(interleave(quasis3, expressions3)).toEqual(
254 | '\n display: $dummyValue; -styled-mixin0: dummyValue; -styled-mixin1: dummyValue;\n'
255 | )
256 | })
257 | })
258 |
259 | describe('reverseString', () => {
260 | const fn = reverseString
261 |
262 | it('reverses a string', () => {
263 | expect(fn('abcd')).toEqual('dcba')
264 | })
265 |
266 | it('handles empty string', () => {
267 | expect(fn('')).toEqual('')
268 | })
269 | })
270 |
271 | describe('nextNonWhitespaceChar', () => {
272 | const fn = nextNonWhitespaceChar
273 |
274 | it('handles empty string', () => {
275 | expect(fn('')).toBe(null)
276 | })
277 |
278 | it('handles all whitespace', () => {
279 | expect(fn(' \t \n \t')).toBe(null)
280 | })
281 |
282 | it('handles no leading whitespace', () => {
283 | expect(fn('abc')).toBe('a')
284 | })
285 |
286 | it('handles spaces', () => {
287 | expect(fn(' b')).toBe('b')
288 | })
289 |
290 | it('handles tabs', () => {
291 | expect(fn('\tc')).toBe('c')
292 | })
293 |
294 | it('handles newlines', () => {
295 | expect(fn('\nd')).toBe('d')
296 | })
297 |
298 | it('handles a mix', () => {
299 | expect(fn(' \n\t\ra \t\r\nb')).toBe('a')
300 | })
301 | })
302 |
303 | describe('isLastDeclarationCompleted', () => {
304 | const fn = isLastDeclarationCompleted
305 |
306 | it('handles all whitespace', () => {
307 | expect(fn(' \n \n \t \r')).toBe(true)
308 | })
309 |
310 | it('handles empty string', () => {
311 | expect(fn('')).toBe(true)
312 | })
313 |
314 | it('handles one-line css', () => {
315 | const prevCSS = 'display: block; color: red '
316 | expect(fn(prevCSS)).toBe(false)
317 |
318 | expect(fn(`${prevCSS};`)).toBe(true)
319 | })
320 |
321 | it('handles multi-line css', () => {
322 | const prevCSS = `
323 | display: block;
324 | color: red`
325 | expect(fn(prevCSS)).toBe(false)
326 |
327 | expect(fn(`${prevCSS};\n`)).toBe(true)
328 | })
329 |
330 | it('handles irregular css', () => {
331 | const prevCSS = `display : block
332 | ; color:
333 | red `
334 | expect(fn(prevCSS)).toBe(false)
335 |
336 | expect(
337 | fn(`${prevCSS}
338 |
339 | ;
340 |
341 | `)
342 | ).toBe(true)
343 | })
344 |
345 | it('handles declaration blocks', () => {
346 | const prevCSS = `
347 | @media screen and (max-width: 600px) {
348 | display: block;
349 | color: red;
350 | }
351 | `
352 | expect(fn(prevCSS)).toBe(true)
353 | })
354 |
355 | it('handles being inside a declaration block', () => {
356 | const prevCSS = `
357 | span {
358 | `
359 | expect(fn(prevCSS)).toBe(true)
360 | })
361 |
362 | it('handles being preceded by a comment', () => {
363 | const prevCSS1 = `
364 | display: block; /* stylelint-disable */
365 | `
366 | expect(fn(prevCSS1)).toBe(true)
367 |
368 | const prevCSS2 = `
369 | display: block;
370 | /* stylelint-disable */
371 | `
372 | expect(fn(prevCSS2)).toBe(true)
373 |
374 | const prevCSS3 = `
375 | display: block;
376 | /*
377 | multiline comment with "*" and "/"
378 | */
379 | `
380 | expect(fn(prevCSS3)).toBe(true)
381 |
382 | const prevCSS4 = `
383 | display: block;
384 | /*
385 | * JSDoc style comment
386 | */
387 | `
388 | expect(fn(prevCSS4)).toBe(true)
389 |
390 | const prevCSS5 = `
391 | display: /* stylelint-disable */
392 | `
393 | expect(fn(prevCSS5)).toBe(false)
394 |
395 | const prevCSS6 = `
396 | display:
397 | /* stylelint-disable */
398 | `
399 | expect(fn(prevCSS6)).toBe(false)
400 |
401 | const prevCSS7 = `
402 | display:
403 | /*
404 | multiline comment with "*" and "/"
405 | */
406 | `
407 | expect(fn(prevCSS7)).toBe(false)
408 |
409 | const prevCSS8 = `
410 | display:
411 | /*
412 | * JSDoc style comment
413 | */
414 | `
415 | expect(fn(prevCSS8)).toBe(false)
416 | })
417 | })
418 |
419 | describe('isStylelintComment', () => {
420 | const fn = isStylelintComment
421 |
422 | it('should match general block ignores', () => {
423 | expect(fn('stylelint-disable')).toBe(true)
424 |
425 | expect(fn('stylelint-enable')).toBe(true)
426 | })
427 |
428 | it('should match block ignores with any arguments', () => {
429 | expect(fn('stylelint-enable some-rule')).toBe(true)
430 |
431 | expect(fn('stylelint-disable asdfsafdsa-fdsafd9a0fd9sa0f asfd8af afdsa7f')).toBe(true)
432 | })
433 |
434 | it("shouldn't match line specific ignores", () => {
435 | expect(fn('stylelint-disable-line')).toBe(false)
436 |
437 | expect(fn('stylelint-disable-next-line')).toBe(false)
438 | })
439 |
440 | it('should handle whitespace in start and end', () => {
441 | expect(fn(' \tstylelint-disable \t')).toBe(true)
442 | })
443 | })
444 |
445 | describe('fixIndentation', () => {
446 | // We only check the one-line case for now as the rest is be covered thoroughly in hard.test.js
447 | it('leaves one-line css alone', () => {
448 | const test1 = 'display: block;'
449 | expect(fixIndentation(test1).text).toBe(test1)
450 |
451 | const test2 = ' display: block;'
452 | expect(fixIndentation(test2).text).toBe(test2)
453 |
454 | const test3 = '\t\tdisplay:block;'
455 | expect(fixIndentation(test3).text).toBe(test3)
456 | })
457 | })
458 |
459 | describe('hasInterpolationTag', () => {
460 | const fn = hasInterpolationTag
461 | it('works for starting comment', () => {
462 | const expression = {
463 | leadingComments: [{ value: ' sc-block ' }],
464 | trailingComments: []
465 | }
466 | expect(fn(expression)).toBe(true)
467 | })
468 |
469 | it('correctly identifies lack of tag', () => {
470 | const expression = {
471 | leadingComments: [{ value: 'some test value' }],
472 | trailingComments: [{ value: 'second test value' }]
473 | }
474 | expect(fn(expression)).toBe(false)
475 | })
476 |
477 | it('handles tag not being first comment', () => {
478 | const expression = {
479 | leadingComments: [{ value: 'some test value' }, { value: 'second test value' }],
480 | trailingComments: [{ value: ' sc-s' }]
481 | }
482 | expect(fn(expression)).toBe(true)
483 | })
484 | })
485 |
486 | describe('parseInterpolationTag', () => {
487 | const fn = parseInterpolationTag
488 | const prepExpression = command => ({
489 | leadingComments: [
490 | { value: 'some test comment' },
491 | {
492 | value: `sc-${command}`,
493 | loc: {
494 | start: {
495 | line: 1,
496 | column: 3
497 | }
498 | }
499 | }
500 | ],
501 | trailingComments: []
502 | })
503 | it('handles the API', () => {
504 | const selectorExpression = prepExpression('selector')
505 | expect(fn(selectorExpression, 1, 'path/to/file')).toBe('.sc-selector1')
506 |
507 | const blockExpression = prepExpression('block')
508 | expect(fn(blockExpression, 1, 'path/to/file')).toBe('-styled-mixin1: dummyValue;')
509 |
510 | const declarationExpression = prepExpression('declaration')
511 | expect(fn(declarationExpression, 1, 'path/to/file')).toBe('-styled-mixin1: dummyValue;')
512 |
513 | const propertyExpression = prepExpression('property')
514 | expect(fn(propertyExpression, 1, 'path/to/file')).toBe('-styled-mixin1')
515 |
516 | const valueExpression = prepExpression('value')
517 | expect(fn(valueExpression, 1, 'path/to/file')).toBe('$dummyValue')
518 |
519 | const customExpression = prepExpression('custom " my test placeholder"')
520 | expect(fn(customExpression, 1, 'path/to/file')).toBe(' my test placeholder')
521 | })
522 |
523 | it('throws on invalid tag', () => {
524 | const invalidExpression = prepExpression('invalid')
525 | expect(fn.bind(null, invalidExpression, 1, 'path/to/file')).toThrow(
526 | /path\/to\/file:1:3:.*invalid sc- tag/
527 | )
528 | })
529 | })
530 |
531 | describe('extrapolateShortenedCommand', () => {
532 | const fn = extrapolateShortenedCommand
533 | const commands = ['hello', 'heaven', 'command']
534 |
535 | it('handles correctly shortened commands', () => {
536 | expect(fn(commands, 'hel')).toBe('hello')
537 | expect(fn(commands, 'hea')).toBe('heaven')
538 | expect(fn(commands, 'c')).toBe('command')
539 | expect(fn(commands, 'comm')).toBe('command')
540 | })
541 |
542 | it('handles full commands', () => {
543 | expect(fn(commands, 'hello')).toBe('hello')
544 | expect(fn(commands, 'heaven')).toBe('heaven')
545 | expect(fn(commands, 'command')).toBe('command')
546 | })
547 |
548 | it('rejects ambigously shortened commands', () => {
549 | expect(fn.bind(this, commands, 'h')).toThrow()
550 | expect(fn.bind(this, commands, 'he', '/path/to/file', { line: 4, column: 6 })).toThrow(
551 | /path\/to\/file:4:6:/
552 | )
553 | })
554 |
555 | it('rejects nonsense', () => {
556 | expect(fn(commands, 'nonsense')).toBeNull()
557 | expect(fn(commands, 'asdfasfd')).toBeNull()
558 | expect(fn(commands, 'x')).toBeNull()
559 | })
560 | })
561 |
562 | describe('extractScTagInformation', () => {
563 | const fn = extractScTagInformation
564 | it('handles normal Sc Tag', () => {
565 | expect(fn(' sc-block ')).toEqual({
566 | command: 'block',
567 | customPlaceholder: undefined
568 | })
569 |
570 | expect(fn('sc-selector')).toEqual({
571 | command: 'selector',
572 | customPlaceholder: undefined
573 | })
574 |
575 | expect(fn('sc-block ')).toEqual({
576 | command: 'block',
577 | customPlaceholder: undefined
578 | })
579 |
580 | expect(fn('sc-block ')).toEqual({
581 | command: 'block',
582 | customPlaceholder: undefined
583 | })
584 | })
585 |
586 | it('handles custom placeholder', () => {
587 | expect(fn(' sc-custom "placeholder test" ')).toEqual({
588 | command: 'custom',
589 | customPlaceholder: 'placeholder test'
590 | })
591 |
592 | expect(fn(" sc-custom 'placeholder test' ")).toEqual({
593 | command: 'custom',
594 | customPlaceholder: 'placeholder test'
595 | })
596 | })
597 | })
598 |
599 | describe('removeBaseIndentation', () => {
600 | const fn = removeBaseIndentation
601 | it('removes indentation correctly', () => {
602 | const inputCss = `
603 | html {
604 | color: red;
605 | }
606 | `
607 | const expectedOutput = `
608 | html {
609 | color: red;
610 | }
611 | `
612 | expect(fn(inputCss)).toBe(expectedOutput)
613 | })
614 |
615 | it('handles oneline css', () => {
616 | const inputCss = ' display: block; '
617 | const expectedOutput = 'display: block;'
618 | expect(fn(inputCss)).toBe(expectedOutput)
619 | })
620 |
621 | it('handles no indentation edge case', () => {
622 | const inputCss = '\ndisplay: block;\ncolor: red;'
623 | const expectedOutput = inputCss
624 | expect(fn(inputCss)).toBe(expectedOutput)
625 | })
626 | })
627 |
628 | describe('isCausedBySubstitution', () => {
629 | const fn = isCausedBySubstitution
630 | const interpolationLines = [
631 | { start: 2, end: 4 },
632 | { start: 5, end: 5 }
633 | ]
634 | it("returns true if real warning line is between some interpolation's start and end", () => {
635 | expect(fn({ rule: 'any rule' }, 3, interpolationLines)).toEqual(true)
636 | })
637 | it("returns false if real warning line is beyond any interpolation's start and end", () => {
638 | expect(fn({ rule: 'any rule' }, 1, interpolationLines)).toEqual(false)
639 | })
640 | it("checks warning rule if real warning line is at some interpolations' start", () => {
641 | expect(fn({ rule: 'comment-empty-line-before' }, 2, interpolationLines)).toEqual(true)
642 | expect(fn({ rule: 'another rule' }, 2, interpolationLines)).toEqual(false)
643 | })
644 | it("checks warning rule if real warning line is at some interpolations' end", () => {
645 | expect(fn({ rule: 'comment-empty-line-before' }, 4, interpolationLines)).toEqual(true)
646 | expect(fn({ rule: 'another rule' }, 4, interpolationLines)).toEqual(false)
647 | })
648 | })
649 |
650 | describe('getCorrectColumn', () => {
651 | const fn = getCorrectColumn
652 | const taggedTemplateLocs = [
653 | { wrappedOffset: 3, start: { line: 1, column: 6 } },
654 | { wrappedOffset: 12, start: { line: 5, column: 39 } }
655 | ]
656 | it('returns identically if the line is not any first line of tagged templates', () => {
657 | expect(fn(taggedTemplateLocs, 4, 1)).toEqual(1)
658 | })
659 | it('returns correctly if the line is some first line of tagged templates', () => {
660 | expect(fn(taggedTemplateLocs, 5, 23)).toEqual(51)
661 | })
662 | })
663 | })
664 |
--------------------------------------------------------------------------------