├── .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 |
477 |
478 |
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 | --------------------------------------------------------------------------------