├── .eslintrc
├── .github
└── workflows
│ ├── node-pretest.yml
│ ├── node.yml
│ ├── rebase.yml
│ └── require-allow-edits.yml
├── .gitignore
├── .npmrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
└── rules
│ ├── no-unused-styles.md
│ └── only-spread-css.md
├── lib
├── index.js
└── rules
│ ├── no-unused-styles.js
│ └── only-spread-css.js
├── package.json
└── tests
├── .eslintrc
├── index.js
└── lib
└── rules
├── no-unused-styles.js
└── only-spread-css.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb-base",
3 | "env": {
4 | "node": true,
5 | },
6 | "rules": {
7 | "strict": 0,
8 | "prefer-destructuring": 0,
9 | "comma-dangle": [2, {
10 | "arrays": "always-multiline",
11 | "objects": "always-multiline",
12 | "imports": "always-multiline",
13 | "exports": "always-multiline",
14 | "functions": "ignore",
15 | }],
16 | "prefer-object-spread": "off",
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/node-pretest.yml:
--------------------------------------------------------------------------------
1 | name: 'Tests: pretest/posttest'
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | tests:
7 | uses: ljharb/actions/.github/workflows/pretest.yml@main
8 |
--------------------------------------------------------------------------------
/.github/workflows/node.yml:
--------------------------------------------------------------------------------
1 | name: 'Tests: node.js'
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | matrix:
7 | runs-on: ubuntu-latest
8 | outputs:
9 | latest: ${{ steps.set-matrix.outputs.requireds }}
10 | minors: ${{ steps.set-matrix.outputs.optionals }}
11 | steps:
12 | - uses: ljharb/actions/node/matrix@main
13 | id: set-matrix
14 | with:
15 | versionsAsRoot: true
16 | type: majors
17 | preset: '>=4' # mocha 5 requires node 4
18 |
19 | latest:
20 | needs: [matrix]
21 | name: 'latest majors'
22 | runs-on: ubuntu-latest
23 |
24 | strategy:
25 | fail-fast: false
26 | matrix:
27 | node-version: ${{ fromJson(needs.matrix.outputs.latest) }}
28 | eslint:
29 | - 8
30 | - 7
31 | - 6
32 | - 5
33 | - 4
34 | - 4.14 # last version without messageId
35 | - 3
36 | exclude:
37 | - node-version: 5
38 | - node-version: 4
39 | - node-version: 15
40 | eslint: 8
41 | - node-version: 13
42 | eslint: 8
43 | - node-version: 11
44 | eslint: 8
45 | - node-version: 11
46 | eslint: 7
47 | - node-version: 10
48 | eslint: 8
49 | - node-version: 9
50 | eslint: 8
51 | - node-version: 9
52 | eslint: 7
53 | - node-version: 8
54 | eslint: 8
55 | - node-version: 8
56 | eslint: 7
57 | - node-version: 7
58 | eslint: 8
59 | - node-version: 7
60 | eslint: 7
61 | - node-version: 7
62 | eslint: 6
63 | - node-version: 6
64 | eslint: 8
65 | - node-version: 6
66 | eslint: 7
67 | - node-version: 6
68 | eslint: 6
69 | - node-version: 5
70 | eslint: 8
71 | - node-version: 5
72 | eslint: 7
73 | - node-version: 5
74 | eslint: 6
75 | - node-version: 5
76 | eslint: 5
77 | - node-version: 4
78 | eslint: 8
79 | - node-version: 4
80 | eslint: 7
81 | - node-version: 4
82 | eslint: 6
83 | - node-version: 4
84 | eslint: 5
85 |
86 | steps:
87 | - uses: actions/checkout@v2
88 | - uses: ljharb/actions/node/install@main
89 | name: 'nvm install ${{ matrix.node-version }} && npm install'
90 | with:
91 | node-version: ${{ matrix.node-version }}
92 | after_install: npm install --no-save "eslint@${{ matrix.eslint }}"
93 | skip-ls-check: ${{ matrix.node-version < 10 && true || false }}
94 | env:
95 | NPM_CONFIG_LEGACY_PEER_DEPS: true
96 | - run: npx ls-engines
97 | if: ${{ matrix.node-version >= 12 }}
98 | - run: npm run tests-only
99 | - uses: codecov/codecov-action@v1
100 |
101 | node:
102 | name: 'node.js'
103 | needs: [latest]
104 | runs-on: ubuntu-latest
105 | steps:
106 | - run: 'echo tests completed'
107 |
--------------------------------------------------------------------------------
/.github/workflows/rebase.yml:
--------------------------------------------------------------------------------
1 | name: Automatic Rebase
2 |
3 | on: [pull_request_target]
4 |
5 | jobs:
6 | _:
7 | name: "Automatic Rebase"
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: ljharb/rebase@master
14 | env:
15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16 |
--------------------------------------------------------------------------------
/.github/workflows/require-allow-edits.yml:
--------------------------------------------------------------------------------
1 | name: Require “Allow Edits”
2 |
3 | on: [pull_request_target]
4 |
5 | jobs:
6 | _:
7 | name: "Require “Allow Edits”"
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: ljharb/require-allow-edits@main
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | # lockfiles
40 | yarn.lock
41 | package-lock.json
42 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v2.4.0
2 | - Add support for eslint v8
3 | - Add engines for node >= 4
4 |
5 | ## v2.3.0
6 | - Add support for eslint v7
7 |
8 | ## v2.2.0
9 | - Add support for eslint v6
10 |
11 | ## v2.1.0
12 | - Add support for eslint v5
13 |
14 | ## v2.0.0
15 | - Remove `findImportCSSFromWithStylesImportDeclaration` and `findRequireCSSFromWithStylesCallExpression` utils
16 | - Remove `cssNoRTL-only`
17 | - Add `react-with-styles/no-unused-styles` to the recommended config as an error
18 | - Restrict the global import of `css`/`cssNoRTL` from `react-with-styles` in the recommended config
19 |
20 | ## v1.1.2
21 | - Remove css global import requirement
22 |
23 | ## v1.1.1
24 |
25 | - Support for ESLint v4.
26 |
27 | ## v1.1.0
28 |
29 | - [New] Add no-unused-styles rule.
30 |
31 | ## v1.0.0
32 |
33 | - Initial release.
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Airbnb
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eslint-plugin-react-with-styles [![Version Badge][npm-version-svg]][package-url]
2 |
3 | [![Build Status][travis-svg]][travis-url]
4 | [![dependency status][deps-svg]][deps-url]
5 | [![dev dependency status][dev-deps-svg]][dev-deps-url]
6 | [![License][license-image]][license-url]
7 | [![Downloads][downloads-image]][downloads-url]
8 |
9 | [![npm badge][npm-badge-png]][package-url]
10 |
11 | ESLint plugin for [react-with-styles][react-with-styles].
12 |
13 | ## Rules
14 |
15 | - [react-with-styles/no-unused-styles](docs/rules/no-unused-styles.md): Require all styles that are defined to be referenced
16 | - [react-with-styles/only-spread-css](docs/rules/only-spread-css.md): Require that `css()` is only spread into a JSX element without a `className` or `style` prop
17 |
18 | [package-url]: https://npmjs.org/package/eslint-plugin-react-with-styles
19 | [npm-version-svg]: http://versionbadg.es/airbnb/eslint-plugin-react-with-styles.svg
20 | [travis-svg]: https://travis-ci.org/airbnb/eslint-plugin-react-with-styles.svg
21 | [travis-url]: https://travis-ci.org/airbnb/eslint-plugin-react-with-styles
22 | [deps-svg]: https://david-dm.org/airbnb/eslint-plugin-react-with-styles.svg
23 | [deps-url]: https://david-dm.org/airbnb/eslint-plugin-react-with-styles
24 | [dev-deps-svg]: https://david-dm.org/airbnb/eslint-plugin-react-with-styles/dev-status.svg
25 | [dev-deps-url]: https://david-dm.org/airbnb/eslint-plugin-react-with-styles#info=devDependencies
26 | [npm-badge-png]: https://nodei.co/npm/eslint-plugin-react-with-styles.png?downloads=true&stars=true
27 | [license-image]: http://img.shields.io/npm/l/eslint-plugin-react-with-styles.svg
28 | [license-url]: LICENSE
29 | [downloads-image]: http://img.shields.io/npm/dm/eslint-plugin-react-with-styles.svg
30 | [downloads-url]: http://npm-stat.com/charts.html?package=eslint-plugin-react-with-styles
31 |
32 | [react-with-styles]: https://github.com/airbnb/react-with-styles
33 |
--------------------------------------------------------------------------------
/docs/rules/no-unused-styles.md:
--------------------------------------------------------------------------------
1 | # Disallow unused styles
2 |
3 | ## Rule Details
4 |
5 | The following patterns are considered warnings:
6 |
7 | ``` jsx
8 | function MyComponent({ styles }) {
9 | return (
10 |
11 | Foo
12 |
13 | );
14 | }
15 |
16 | export default withStyles(() => ({
17 | foo: {
18 | backgroundColor: 'red',
19 | },
20 |
21 | bar: { // <--- this style is not used
22 | backgroundColor: 'green',
23 | },
24 | }))(MyComponent);
25 | ```
26 |
27 | The following patterns are not warnings:
28 |
29 | ``` jsx
30 | function MyComponent({ styles }) {
31 | return (
32 |
33 | Foo
34 |
35 | );
36 | }
37 |
38 | export default withStyles(() => ({
39 | foo: {
40 | backgroundColor: 'red',
41 | },
42 | }))(MyComponent);
43 | ```
44 |
45 | ## Known limitations
46 |
47 | - Will not detect styles defined by computed properties.
48 | - Will not detect styles defined by object spread.
49 | - Will not handle files that contain multiple styled components very well.
50 | - Will not handle `styles` prop that has been renamed to something else.
51 |
--------------------------------------------------------------------------------
/docs/rules/only-spread-css.md:
--------------------------------------------------------------------------------
1 | # Require that `css()` is only spread into a JSX element without a `className` or `style` prop
2 |
3 | The shape of the object returned by the `css()` function from withStyles needs to be opaque in order to give us maximum interoperability with different style systems (e.g. Aphrodite or React Native). It may provide `className`, `style`, or both, so you cannot use these props if you are using `css()`.
4 |
5 | If you need to add some inline styles (e.g. a style that depends on a non-enumerable value of a prop), `css()` can accept plain objects (example below).
6 |
7 | ## Rule details
8 |
9 | The following patterns are considered warnings:
10 |
11 | ```jsx
12 | import { css } from 'withStyles';
13 |
14 | ```
15 |
16 | ```jsx
17 | import { css } from 'withStyles';
18 |
19 | ```
20 |
21 | ```jsx
22 | import { css } from 'withStyles';
23 |
24 | ```
25 |
26 | ```jsx
27 | import { css } from 'withStyles';
28 | const { className, style } = css(styles.foo);
29 |
30 | ```
31 |
32 | The following patterns are not warnings:
33 |
34 | ```jsx
35 | import { css } from 'withStyles';
36 |
37 | ```
38 |
39 | ```jsx
40 | import { css } from 'withStyles';
41 |
42 | ```
43 |
44 | ## Known limitations
45 |
46 | - Does not try to check for the shape of other objects that are spread onto the element.
47 |
48 | ```jsx
49 | const bar = { className: 'foo' };
50 |
51 | ```
52 |
53 | - Does not keep track of assigning the `css()` function to a different variable.
54 |
55 | ```jsx
56 | import { css } from 'withStyles';
57 | const withStylesCSS = css;
58 |
59 | ```
60 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const onlySpreadCSS = require('./rules/only-spread-css');
4 | const noUnusedStyles = require('./rules/no-unused-styles');
5 |
6 | module.exports = {
7 | rules: {
8 | 'only-spread-css': onlySpreadCSS,
9 | 'no-unused-styles': noUnusedStyles,
10 | },
11 |
12 | configs: {
13 | recommended: {
14 | rules: {
15 | 'react-with-styles/only-spread-css': 'error',
16 | 'react-with-styles/no-unused-styles': 'error',
17 | 'no-restricted-imports': ['error', {
18 | paths: [{
19 | name: 'react-with-styles',
20 | importNames: ['css', 'cssNoRTL'],
21 | message: 'The global `css` and `cssNoRTL` exports are deprecated. Please use `this.props.css` instead!',
22 | }],
23 | }],
24 | },
25 | },
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/lib/rules/no-unused-styles.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const has = require('has');
4 |
5 | function getBasicIdentifier(node) {
6 | if (node.type === 'Identifier') {
7 | // styles.foo
8 | return node.name;
9 | }
10 |
11 | if (node.type === 'Literal') {
12 | // styles['foo']
13 | return node.value;
14 | }
15 |
16 | if (node.type === 'TemplateLiteral') {
17 | // styles[`foo`]
18 | if (node.expressions.length) {
19 | // styles[`foo${bar}`]
20 | return null;
21 | }
22 | return node.quasis[0].value.raw;
23 | }
24 |
25 | // Might end up here with thigs like:
26 | // styles['foo' + bar]
27 | return null;
28 | }
29 |
30 | module.exports = {
31 | meta: {
32 | docs: {
33 | description: 'Require that all styles that are defined are also referenced in the same file',
34 | recommended: true,
35 | },
36 |
37 | schema: [],
38 | },
39 |
40 | create: function rule(context) {
41 | const usedStyles = {};
42 | const definedStyles = {};
43 |
44 | return {
45 | CallExpression(node) {
46 | if (node.callee.name === 'withStyles') {
47 | const styles = node.arguments[0];
48 |
49 | if (styles && styles.type === 'ArrowFunctionExpression') {
50 | const body = styles.body;
51 |
52 | let stylesObj;
53 | if (body.type === 'ObjectExpression') {
54 | stylesObj = body;
55 | } else if (body.type === 'BlockStatement') {
56 | body.body.forEach((bodyNode) => {
57 | if (
58 | bodyNode.type === 'ReturnStatement'
59 | && bodyNode.argument.type === 'ObjectExpression'
60 | ) {
61 | stylesObj = bodyNode.argument;
62 | }
63 | });
64 | }
65 |
66 | if (stylesObj) {
67 | stylesObj.properties.forEach((property) => {
68 | if (property.computed) {
69 | // Skip over computed properties for now.
70 | // e.g. `{ [foo]: { ... } }`
71 | // TODO handle this better?
72 | return;
73 | }
74 |
75 | if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
76 | // Skip over object spread for now.
77 | // e.g. `{ ...foo }`
78 | // TODO handle this better?
79 | return;
80 | }
81 |
82 | definedStyles[property.key.name] = property;
83 | });
84 | }
85 | }
86 |
87 | // Now we know all of the defined styles and used styles, so we can
88 | // see if there are any defined styles that are not used.
89 | Object.keys(definedStyles).forEach((definedStyleKey) => {
90 | if (!has(usedStyles, definedStyleKey)) {
91 | context.report(
92 | definedStyles[definedStyleKey],
93 | `Style \`${definedStyleKey}\` is unused`
94 | );
95 | }
96 | });
97 | }
98 | },
99 |
100 | MemberExpression(node) {
101 | if (node.object.type === 'Identifier' && node.object.name === 'styles') {
102 | const style = getBasicIdentifier(node.property);
103 | if (style) {
104 | usedStyles[style] = true;
105 | }
106 | return;
107 | }
108 |
109 | const stylesIdentifier = getBasicIdentifier(node.property);
110 | if (!stylesIdentifier) {
111 | // props['foo' + bar].baz
112 | return;
113 | }
114 |
115 | if (stylesIdentifier !== 'styles') {
116 | // props.foo.bar
117 | return;
118 | }
119 |
120 | const parent = node.parent;
121 |
122 | if (parent.type !== 'MemberExpression') {
123 | // foo.styles
124 | return;
125 | }
126 |
127 | if (node.object.object && node.object.object.type !== 'ThisExpression') {
128 | // foo.foo.styles
129 | return;
130 | }
131 |
132 | const propsIdentifier = getBasicIdentifier(parent.object);
133 | if (propsIdentifier && propsIdentifier !== 'props') {
134 | return;
135 | }
136 | if (!propsIdentifier && parent.object.type !== 'MemberExpression') {
137 | return;
138 | }
139 |
140 | if (parent.parent.type === 'MemberExpression') {
141 | // this.props.props.styles
142 | return;
143 | }
144 |
145 | const style = getBasicIdentifier(parent.property);
146 | if (style) {
147 | usedStyles[style] = true;
148 | }
149 | },
150 | };
151 | },
152 | };
153 |
--------------------------------------------------------------------------------
/lib/rules/only-spread-css.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | meta: {
5 | docs: {
6 | description: 'Require that css() is only spread into a JSX element without a `className` or `style` prop',
7 | recommended: true,
8 | },
9 |
10 | schema: [],
11 | },
12 |
13 | create: function rule(context) {
14 | const CSS_METHOD_NAME = 'css';
15 | return {
16 | CallExpression(node) {
17 | if (node.callee.name !== CSS_METHOD_NAME) {
18 | // foo()
19 | return;
20 | }
21 |
22 | if (node.parent.type === 'JSXSpreadAttribute') {
23 | //
24 | return;
25 | }
26 |
27 | context.report(
28 | node,
29 | `Only spread \`${CSS_METHOD_NAME}()\` directly into an element, e.g. \`\`.`
30 | );
31 | },
32 |
33 | JSXSpreadAttribute(node) {
34 | if (node.argument.type !== 'CallExpression') {
35 | //
36 | //
37 | // TODO make this work for
38 | // const foo = css(bar);
39 | //
40 | return;
41 | }
42 |
43 | if (node.argument.callee.name !== CSS_METHOD_NAME) {
44 | //
45 | return;
46 | }
47 |
48 | // At this point, we know that this JSX is using `css()` from
49 | // withStyles, so let's see if it is also using `className` or `style`.
50 | node.parent.attributes.forEach((attribute) => {
51 | if (attribute === node) {
52 | // This is the `{...css(foo)}` bit, so let's skip it.
53 | return;
54 | }
55 |
56 | if (attribute.type === 'JSXSpreadAttribute') {
57 | // TODO dig into other spread things to see if we can find a
58 | // `className` or `style` prop. This would require a bunch of manual
59 | // bookkeeping about variables and scopes.
60 | //
61 | // e.g.
62 | //
63 | // const foo = { className: 'hi' };
64 | //
65 | return;
66 | }
67 |
68 | if (attribute.name.name === 'className') {
69 | context.report(
70 | attribute,
71 | `Do not use \`className\` with \`{...${CSS_METHOD_NAME}()}\`.`
72 | );
73 | return;
74 | }
75 |
76 | if (attribute.name.name === 'style') {
77 | context.report(
78 | attribute,
79 | `Do not use \`style\` with \`{...${CSS_METHOD_NAME}()}\`.`
80 | );
81 | }
82 | });
83 | },
84 | };
85 | },
86 | };
87 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-react-with-styles",
3 | "version": "2.4.0",
4 | "description": "ESLint plugin for react-with-styles",
5 | "main": "lib/index.js",
6 | "directories": {
7 | "doc": "docs",
8 | "test": "tests"
9 | },
10 | "scripts": {
11 | "check-changelog": "expr $(git status --porcelain 2>/dev/null| grep \"^\\s*M.*CHANGELOG.md\" | wc -l) >/dev/null || (echo 'Please edit CHANGELOG.md' && exit 1)",
12 | "check-only-changelog-changed": "(expr $(git status --porcelain 2>/dev/null| grep -v \"CHANGELOG.md\" | wc -l) >/dev/null && echo 'Only CHANGELOG.md may have uncommitted changes' && exit 1) || exit 0",
13 | "lint": "eslint .",
14 | "mocha": "mocha --recursive",
15 | "postversion": "git commit package.json CHANGELOG.md -m \"Version $npm_package_version\" && npm run tag && git push && git push --tags && npm publish",
16 | "prepublishOnly": "safe-publish-latest",
17 | "prepublish": "not-in-publish || safe-publish-latest",
18 | "pretest": "npm run --silent lint",
19 | "preversion": "npm run test && npm run check-changelog && npm run check-only-changelog-changed",
20 | "tag": "git tag v$npm_package_version",
21 | "test": "npm run tests-only",
22 | "tests-only": "npm run mocha --silent tests",
23 | "posttest": "aud --production",
24 | "version:major": "npm --no-git-tag-version version major",
25 | "version:minor": "npm --no-git-tag-version version minor",
26 | "version:patch": "npm --no-git-tag-version version patch"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/airbnb/eslint-plugin-react-with-styles.git"
31 | },
32 | "keywords": [
33 | "eslint",
34 | "react",
35 | "react-with-styles",
36 | "eslint-plugin",
37 | "eslintplugin"
38 | ],
39 | "author": "Joe Lencioni ",
40 | "license": "MIT",
41 | "bugs": {
42 | "url": "https://github.com/airbnb/eslint-plugin-react-with-styles/issues"
43 | },
44 | "homepage": "https://github.com/airbnb/eslint-plugin-react-with-styles#readme",
45 | "peerDependencies": {
46 | "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8"
47 | },
48 | "devDependencies": {
49 | "aud": "^2.0.0",
50 | "chai": "^4.2.0",
51 | "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8",
52 | "eslint-config-airbnb-base": "^15.0.0",
53 | "eslint-plugin-import": "^2.25.4",
54 | "in-publish": "^2.0.0",
55 | "ls-engines": "^0.6.4",
56 | "mocha": "^5.2.0",
57 | "safe-publish-latest": "^1.1.3",
58 | "semver": "^6.3.0"
59 | },
60 | "dependencies": {
61 | "has": "^1.0.3"
62 | },
63 | "engines": {
64 | "node": ">= 4"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tests/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const expect = require('chai').expect;
4 | const fs = require('fs');
5 | const path = require('path');
6 |
7 | const plugin = require('..');
8 |
9 | const ruleFiles = fs.readdirSync(path.resolve(__dirname, '../lib/rules/'))
10 | .map((f) => path.basename(f, '.js'));
11 |
12 | describe('all rule files are exported by the plugin', () => {
13 | ruleFiles.forEach((ruleName) => {
14 | it(`exports ${ruleName}`, () => {
15 | expect(plugin.rules[ruleName])
16 | // eslint-disable-next-line global-require, import/no-dynamic-require
17 | .to.eql(require(path.join('../lib/rules', ruleName)));
18 | });
19 | });
20 | });
21 |
22 | describe('configurations', () => {
23 | it('exports a "recommended" configuration', () => {
24 | expect(plugin.configs.recommended).to.not.equal(null);
25 | expect(plugin.configs.recommended).to.not.equal(undefined);
26 | expect(plugin.configs.recommended).to.not.be.eql({});
27 | });
28 |
29 | it('has rules in the "recommended" configuration', () => {
30 | expect(Object.keys(plugin.configs.recommended).length).to.be.above(0);
31 | });
32 |
33 | it('has correctly-formatted rule names in the "recommended" configuration', () => {
34 | Object.keys(plugin.configs.recommended.rules).forEach((configName) => {
35 | if (configName === 'no-restricted-imports') return;
36 | expect(configName.startsWith('react-with-styles/')).to.equal(true);
37 | });
38 | });
39 |
40 | it('has synchronized recommended metadata', () => {
41 | ruleFiles.forEach((ruleName) => {
42 | const fullRuleName = `react-with-styles/${ruleName}`;
43 | const inRecommendedConfig = Boolean(plugin.configs.recommended.rules[fullRuleName]);
44 | const isRecommended = plugin.rules[ruleName].meta.docs.recommended;
45 | if (inRecommendedConfig) {
46 | expect(isRecommended, `${ruleName} metadata should mark it as recommended`)
47 | .to.equal(true);
48 | } else {
49 | expect(isRecommended, `${ruleName} metadata should not mark it as recommended`)
50 | .to.equal(false);
51 | }
52 | });
53 | });
54 |
55 | it('has all "recommended" rule names that match rule names', () => {
56 | Object.keys(plugin.configs.recommended.rules).forEach((configName) => {
57 | if (configName === 'no-restricted-imports') return;
58 | const ruleName = configName.substring('react-with-styles/'.length);
59 | const rule = plugin.rules[ruleName];
60 |
61 | expect(rule).to.not.equal(null);
62 | expect(rule).to.not.equal(undefined);
63 | expect(rule).to.not.eql({});
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-unused-styles.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Require that all styles that are defined are also referenced in the same file
3 | * @author Joe Lencioni
4 | */
5 |
6 | 'use strict';
7 |
8 | const RuleTester = require('eslint').RuleTester;
9 | const semver = require('semver');
10 | const eslintVersion = require('eslint/package.json').version;
11 |
12 | const rule = require('../../../lib/rules/no-unused-styles');
13 |
14 | const parserOptions = {
15 | ecmaVersion: 6,
16 | ecmaFeatures: {
17 | jsx: true,
18 | },
19 | sourceType: 'module',
20 | };
21 |
22 | const ruleTester = new RuleTester();
23 | ruleTester.run('no-unused-styles', rule, {
24 | valid: [].concat(
25 | {
26 | parserOptions,
27 | code: `
28 | import { css } from 'withStyles';
29 |
30 | `.trim(),
31 | },
32 |
33 | {
34 | parserOptions,
35 | code: `
36 | import { css } from 'withStyles';
37 | const foo = props.styles;
38 | `.trim(),
39 | },
40 |
41 | {
42 | parserOptions,
43 | code: `
44 | import { css } from 'withStyles';
45 |
46 | function Foo({ styles }) {
47 | return (
48 |
49 | );
50 | }
51 |
52 | export default withStyles(() => ({
53 | foo: {},
54 | }))(Foo);
55 | `.trim(),
56 | },
57 |
58 | {
59 | parserOptions,
60 | code: `
61 | import { css } from 'withStyles';
62 |
63 | function Foo({ styles }) {
64 | return (
65 |
66 | );
67 | }
68 |
69 | export default withStyles(() => ({
70 | foo: {},
71 | }))(Foo);
72 | `.trim(),
73 | },
74 |
75 | {
76 | parserOptions,
77 | code: `
78 | import { css } from 'withStyles';
79 |
80 | function Foo({ styles }) {
81 | return (
82 |
83 | );
84 | }
85 |
86 | export default withStyles(() => ({
87 | foo: {},
88 | }))(Foo);
89 | `.trim(),
90 | },
91 |
92 | {
93 | parserOptions,
94 | code: `
95 | import { css } from 'withStyles';
96 |
97 | function Foo(props) {
98 | return (
99 |
100 | );
101 | }
102 |
103 | export default withStyles(() => ({
104 | foo: {},
105 | }))(Foo);
106 | `.trim(),
107 | },
108 |
109 | {
110 | parserOptions,
111 | code: `
112 | import { css } from 'withStyles';
113 |
114 | function Foo(props) {
115 | return (
116 |
117 | );
118 | }
119 |
120 | export default withStyles(() => ({
121 | foo: {},
122 | }))(Foo);
123 | `.trim(),
124 | },
125 |
126 | {
127 | parserOptions,
128 | code: `
129 | import { css } from 'withStyles';
130 |
131 | function Foo(props) {
132 | return (
133 |
134 | );
135 | }
136 |
137 | export default withStyles(() => ({
138 | foo: {},
139 | }))(Foo);
140 | `.trim(),
141 | },
142 |
143 | {
144 | parserOptions,
145 | code: `
146 | import { css } from 'withStyles';
147 |
148 | function Foo(props) {
149 | return (
150 |
151 | );
152 | }
153 |
154 | export default withStyles(() => ({
155 | foo: {},
156 | }))(Foo);
157 | `.trim(),
158 | },
159 |
160 | {
161 | parserOptions,
162 | code: `
163 | import { css } from 'withStyles';
164 |
165 | function Foo(props) {
166 | return (
167 |
168 | );
169 | }
170 |
171 | export default withStyles(() => ({
172 | foo: {},
173 | }))(Foo);
174 | `.trim(),
175 | },
176 |
177 | {
178 | parserOptions,
179 | code: `
180 | import { css } from 'withStyles';
181 |
182 | function Foo(props) {
183 | return (
184 |
185 | );
186 | }
187 |
188 | export default withStyles(() => ({
189 | foo: {},
190 | }))(Foo);
191 | `.trim(),
192 | },
193 |
194 | {
195 | parserOptions,
196 | code: `
197 | import { css } from 'withStyles';
198 |
199 | function Foo(props) {
200 | return (
201 |
202 | );
203 | }
204 |
205 | export default withStyles(() => ({
206 | foo: {},
207 | }))(Foo);
208 | `.trim(),
209 | },
210 |
211 | {
212 | parserOptions,
213 | code: `
214 | import { css } from 'withStyles';
215 |
216 | class Foo extends React.Component {
217 | render() {
218 | return ;
219 | }
220 | }
221 |
222 | export default withStyles(() => ({
223 | foo: {},
224 | }))(Foo);
225 | `.trim(),
226 | },
227 |
228 | {
229 | parserOptions,
230 | code: `
231 | import { css } from 'withStyles';
232 |
233 | class Foo extends React.Component {
234 | render() {
235 | return ;
236 | }
237 | }
238 |
239 | export default withStyles(() => ({
240 | foo: {},
241 | }))(Foo);
242 | `.trim(),
243 | },
244 |
245 | {
246 | parserOptions,
247 | code: `
248 | import { css } from 'withStyles';
249 |
250 | class Foo extends React.Component {
251 | render() {
252 | return ;
253 | }
254 | }
255 |
256 | export default withStyles(() => ({
257 | foo: {},
258 | }))(Foo);
259 | `.trim(),
260 | },
261 |
262 | {
263 | parserOptions,
264 | code: `
265 | import { css } from 'withStyles';
266 |
267 | class Foo extends React.Component {
268 | render() {
269 | return ;
270 | }
271 | }
272 |
273 | export default withStyles(() => ({
274 | foo: {},
275 | }))(Foo);
276 | `.trim(),
277 | },
278 |
279 | {
280 | parserOptions,
281 | code: `
282 | import { css } from 'withStyles';
283 |
284 | class Foo extends React.Component {
285 | render() {
286 | return ;
287 | }
288 | }
289 |
290 | export default withStyles(() => ({
291 | foo: {},
292 | }))(Foo);
293 | `.trim(),
294 | },
295 |
296 | {
297 | parserOptions,
298 | code: `
299 | import { css } from 'withStyles';
300 |
301 | class Foo extends React.Component {
302 | render() {
303 | return ;
304 | }
305 | }
306 |
307 | export default withStyles(() => ({
308 | foo: {},
309 | }))(Foo);
310 | `.trim(),
311 | },
312 |
313 | {
314 | parserOptions,
315 | code: `
316 | import { css } from 'withStyles';
317 |
318 | class Foo extends React.Component {
319 | render() {
320 | return ;
321 | }
322 | }
323 |
324 | export default withStyles(() => ({
325 | foo: {},
326 | }))(Foo);
327 | `.trim(),
328 | },
329 |
330 | {
331 | parserOptions,
332 | code: `
333 | import { css } from 'withStyles';
334 |
335 | class Foo extends React.Component {
336 | render() {
337 | return ;
338 | }
339 | }
340 |
341 | export default withStyles(() => ({
342 | foo: {},
343 | }))(Foo);
344 | `.trim(),
345 | },
346 |
347 | {
348 | parserOptions,
349 | code: `
350 | import { css } from 'withStyles';
351 |
352 | class Foo extends React.Component {
353 | render() {
354 | return ;
355 | }
356 | }
357 |
358 | export default withStyles(() => ({
359 | foo: {},
360 | }))(Foo);
361 | `.trim(),
362 | },
363 |
364 | {
365 | parserOptions,
366 | code: `
367 | import { css } from 'withStyles';
368 |
369 | class Foo extends React.Component {
370 | render() {
371 | const { styles } = this.props;
372 | return ;
373 | }
374 | }
375 |
376 | export default withStyles(() => ({
377 | foo: {},
378 | }))(Foo);
379 | `.trim(),
380 | },
381 |
382 | {
383 | parserOptions,
384 | code: `
385 | import { css } from 'withStyles';
386 |
387 | function Foo({ styles }) {
388 | const something = isActive ? styles.foo : null;
389 | return ;
390 | }
391 |
392 | export default withStyles(() => ({
393 | foo: {},
394 | }))(Foo);
395 | `.trim(),
396 | },
397 |
398 | { // TODO handle computed properties better?
399 | parserOptions,
400 | code: `
401 | import { css } from 'withStyles';
402 |
403 | function Foo({ styles }) {
404 | return (
405 |
406 | );
407 | }
408 |
409 | export default withStyles(() => ({
410 | [foo]: {},
411 | }))(Foo);
412 | `.trim(),
413 | },
414 |
415 | semver.satisfies('>= 5', eslintVersion) ? { // TODO handle object spread better?
416 | parserOptions: Object.assign({}, parserOptions, { ecmaVersion: 2019 }),
417 | code: `
418 | import { css } from 'withStyles';
419 |
420 | function Foo({ styles }) {
421 | return (
422 |
423 | );
424 | }
425 |
426 | export default withStyles(() => ({
427 | ...foo,
428 | }))(Foo);
429 | `.trim(),
430 | } : [],
431 |
432 | {
433 | parserOptions,
434 | code: `
435 | import { css } from 'withStyles';
436 |
437 | function Foo({ styles }) {
438 | return (
439 |
440 | );
441 | }
442 |
443 | export default withStyles(() => {
444 | return {
445 | foo: {},
446 | }
447 | })(Foo);
448 | `.trim(),
449 | },
450 |
451 | {
452 | parserOptions,
453 | code: `
454 | import { css } from 'withStyles';
455 |
456 | function Foo({ styles }) {
457 | return (
458 |
459 | );
460 | }
461 |
462 | export default withStyles(() => ({
463 | foo: {},
464 | bar: {},
465 | }))(Foo);
466 | `.trim(),
467 | },
468 |
469 | {
470 | parserOptions,
471 | code: `
472 | import { css } from 'withStyles';
473 |
474 | function Foo({ styles }) {
475 | return (
476 |
479 | );
480 | }
481 |
482 | export default withStyles(() => ({
483 | foo: {},
484 | bar: {},
485 | }))(Foo);
486 | `.trim(),
487 | },
488 |
489 | {
490 | parserOptions,
491 | code: `
492 | function Foo({ css, styles }) {
493 | return (
494 |
495 | );
496 | }
497 |
498 | export default withStyles(() => ({
499 | foo: {},
500 | }))(Foo);
501 | `.trim(),
502 | }
503 | ),
504 |
505 | invalid: [
506 | {
507 | parserOptions,
508 | code: `
509 | import { css } from 'withStyles';
510 |
511 | function Foo({ styles }) {
512 | return (
513 |
514 | );
515 | }
516 |
517 | export default withStyles(() => ({
518 | foo: {},
519 | bar: {},
520 | }))(Foo);
521 | `.trim(),
522 | errors: [{
523 | message: 'Style `bar` is unused',
524 | type: 'Property',
525 | }],
526 | },
527 |
528 | {
529 | parserOptions,
530 | code: `
531 | import { css } from 'withStyles';
532 |
533 | function Foo({ styles }) {
534 | return (
535 |
536 | );
537 | }
538 |
539 | export default withStyles(() => {
540 | return {
541 | foo: {},
542 | bar: {},
543 | }
544 | })(Foo);
545 | `.trim(),
546 | errors: [{
547 | message: 'Style `bar` is unused',
548 | type: 'Property',
549 | }],
550 | },
551 |
552 | {
553 | parserOptions,
554 | code: `
555 | import { css } from 'withStyles';
556 |
557 | class Foo extends React.Component {
558 | render() {
559 | return (
560 |
561 | );
562 | }
563 | }
564 |
565 | export default withStyles(() => ({
566 | foo: {},
567 | }))(Foo);
568 | `.trim(),
569 | errors: [{
570 | message: 'Style `foo` is unused',
571 | type: 'Property',
572 | }],
573 | },
574 |
575 | {
576 | parserOptions,
577 | code: `
578 | import { css } from 'withStyles';
579 |
580 | function Foo(props) {
581 | return (
582 |
583 | );
584 | }
585 |
586 | export default withStyles(() => ({
587 | foo: {},
588 | }))(Foo);
589 | `.trim(),
590 | errors: [{
591 | message: 'Style `foo` is unused',
592 | type: 'Property',
593 | }],
594 | },
595 |
596 | {
597 | parserOptions,
598 | code: `
599 | import { css } from 'withStyles';
600 |
601 | function Foo(props) {
602 | return (
603 |
604 | );
605 | }
606 |
607 | export default withStyles(() => ({
608 | bar: {},
609 | }))(Foo);
610 | `.trim(),
611 | errors: [{
612 | message: 'Style `bar` is unused',
613 | type: 'Property',
614 | }],
615 | },
616 |
617 | {
618 | parserOptions,
619 | code: `
620 | import { css } from 'withStyles';
621 |
622 | function Foo(props) {
623 | return (
624 |
625 | );
626 | }
627 |
628 | export default withStyles(() => ({
629 | foo: {},
630 | }))(Foo);
631 | `.trim(),
632 | errors: [{
633 | message: 'Style `foo` is unused',
634 | type: 'Property',
635 | }],
636 | },
637 |
638 | {
639 | parserOptions,
640 | code: `
641 | function Foo({ css, styles }) {
642 | return (
643 |
644 | );
645 | }
646 |
647 | export default withStyles(() => ({
648 | foo: {},
649 | bar: {},
650 | }))(Foo);
651 | `.trim(),
652 | errors: [{
653 | message: 'Style `bar` is unused',
654 | type: 'Property',
655 | }],
656 | },
657 | ],
658 | });
659 |
--------------------------------------------------------------------------------
/tests/lib/rules/only-spread-css.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Prevent usage of `{...css(styles.foo)}` with `className` or
3 | * `style` props.
4 | * @author Joe Lencioni
5 | */
6 |
7 | 'use strict';
8 |
9 | const RuleTester = require('eslint').RuleTester;
10 |
11 | const rule = require('../../../lib/rules/only-spread-css');
12 |
13 | const parserOptions = {
14 | ecmaVersion: 6,
15 | ecmaFeatures: {
16 | jsx: true,
17 | },
18 | sourceType: 'module',
19 | };
20 |
21 | const ruleTester = new RuleTester();
22 | ruleTester.run('only-spread-css', rule, {
23 |
24 | valid: [
25 | {
26 | parserOptions,
27 | code: `
28 |
29 | `.trim(),
30 | },
31 |
32 | {
33 | parserOptions,
34 | code: `
35 | import { css } from 'withStyles';
36 |
37 | `.trim(),
38 | },
39 |
40 | {
41 | parserOptions,
42 | code: `
43 | import { css } from 'withStyles';
44 | const bar = { baz: true };
45 |
46 | `.trim(),
47 | },
48 |
49 | {
50 | parserOptions,
51 | code: `
52 | import { css } from 'withStyles';
53 | import { bar } from 'somethingElse';
54 |
55 | `.trim(),
56 | },
57 | ],
58 |
59 | invalid: [
60 | {
61 | parserOptions,
62 | code: `
63 | import { css } from 'airbnb-dls-web/build/themes/withStyles';
64 |
65 | `.trim(),
66 | errors: [{
67 | message: 'Do not use `className` with `{...css()}`.',
68 | type: 'JSXAttribute',
69 | }],
70 | },
71 |
72 | {
73 | parserOptions,
74 | code: `
75 | import { css } from '../../themes/withStyles';
76 |
77 | `.trim(),
78 | errors: [{
79 | message: 'Do not use `className` with `{...css()}`.',
80 | type: 'JSXAttribute',
81 | }],
82 | },
83 |
84 | {
85 | parserOptions,
86 | code: `
87 | import { css } from 'withStyles';
88 |
89 | `.trim(),
90 | errors: [{
91 | message: 'Do not use `className` with `{...css()}`.',
92 | type: 'JSXAttribute',
93 | }],
94 | },
95 |
96 | {
97 | parserOptions,
98 | code: `
99 | import { css } from 'withStyles';
100 |
101 | `.trim(),
102 | errors: [{
103 | message: 'Do not use `className` with `{...css()}`.',
104 | type: 'JSXAttribute',
105 | }],
106 | },
107 |
108 | {
109 | parserOptions,
110 | code: `
111 | import { css } from 'withStyles';
112 |
113 | `.trim(),
114 | errors: [{
115 | message: 'Do not use `style` with `{...css()}`.',
116 | type: 'JSXAttribute',
117 | }],
118 | },
119 |
120 | {
121 | parserOptions,
122 | code: `
123 | import { css } from 'withStyles';
124 | const { style } = css(foo);
125 | `.trim(),
126 | errors: [{
127 | message: 'Only spread `css()` directly into an element, e.g. ``.',
128 | type: 'CallExpression',
129 | }],
130 | },
131 |
132 | {
133 | parserOptions,
134 | code: `
135 | import { css } from 'withStyles';
136 |
137 | `.trim(),
138 | errors: [{
139 | message: 'Only spread `css()` directly into an element, e.g. ``.',
140 | type: 'CallExpression',
141 | }],
142 | },
143 |
144 | {
145 | parserOptions,
146 | code: `
147 | import { css } from 'withStyles';
148 |
149 | `.trim(),
150 | errors: [{
151 | message: 'Only spread `css()` directly into an element, e.g. ``.',
152 | type: 'CallExpression',
153 | }],
154 | },
155 |
156 | {
157 | parserOptions,
158 | code: `
159 | import { css } from 'withStyles';
160 |
161 | `.trim(),
162 | errors: [
163 | {
164 | message: 'Do not use `className` with `{...css()}`.',
165 | type: 'JSXAttribute',
166 | },
167 | {
168 | message: 'Do not use `style` with `{...css()}`.',
169 | type: 'JSXAttribute',
170 | },
171 | ],
172 | },
173 |
174 | {
175 | parserOptions,
176 | code: `
177 | const { css } = require('withStyles');
178 |
179 | `.trim(),
180 | errors: [{
181 | message: 'Do not use `className` with `{...css()}`.',
182 | type: 'JSXAttribute',
183 | }],
184 | },
185 |
186 | {
187 | parserOptions,
188 | code: `
189 | const { css } = require('withStyles');
190 |
191 | `.trim(),
192 | errors: [{
193 | message: 'Do not use `style` with `{...css()}`.',
194 | type: 'JSXAttribute',
195 | }],
196 | },
197 | ],
198 | });
199 |
--------------------------------------------------------------------------------