├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── assets └── banner.png ├── docs └── rules │ ├── no-unused-styles.md │ └── sort-styles.md ├── index.js ├── lib ├── rules │ ├── no-unused-styles.js │ └── sort-styles.js ├── types.d.ts └── util │ ├── Components.js │ └── stylesheet.js ├── package-lock.json ├── package.json └── tests ├── index.js └── lib └── rules ├── no-unused-styles.js └── test-one-case.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "settings": { 4 | "react": { 5 | "version": "detect" 6 | } 7 | }, 8 | "rules": { 9 | "strict": [0, "global"], 10 | "func-names": 0, 11 | "object-shorthand": 0, 12 | "consistent-return": 0, 13 | "prefer-template": 0, 14 | "comma-dangle": ["error", { 15 | "arrays": "always-multiline", 16 | "objects": "always-multiline", 17 | "imports": "always-multiline", 18 | "exports": "always-multiline", 19 | "functions": "never" 20 | }] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | .idea 30 | 31 | # OSX 32 | # 33 | .DS_Store 34 | 35 | # Nyc/istanbul 36 | .nyc_output -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 RodSarhan 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-native-unistyles 2 | 3 | # ESLint plugin for React Native Unistyles 4 | 5 | ![NPM Downloads](https://img.shields.io/npm/d18m/eslint-plugin-react-native-unistyles) [![GitHub Repo stars](https://img.shields.io/github/stars/RodSarhan/eslint-plugin-react-native-unistyles?style=social)](https://github.com/RodSarhan/eslint-plugin-react-native-unistyles) ![NPM Version](https://img.shields.io/npm/v/eslint-plugin-react-native-unistyles) [![License](https://img.shields.io/github/license/RodSarhan/eslint-plugin-react-native-unistyles)](https://github.com/RodSarhan/eslint-plugin-react-native-unistyles/blob/main/LICENSE) 6 | 7 | [React Native Unistyles](https://github.com/jpudysz/react-native-unistyles) linting rules for ESLint. This repository is structured like (and contains code from) [eslint-plugin-react-native](https://github.com/Intellicode/eslint-plugin-react-native). 8 | 9 | ## Supported Versions 10 | 11 | This plugin only supports Unistyles v2 for now 12 | 13 | I am not currently using unistyles due to a change in my career, so I won't be working on v3 support in the foreseeable future 14 | 15 | You're welcome to open a PR or take over the project if you'd like. 16 | 17 | ## Installation 18 | 19 | Install eslint-plugin-react-native-unistyles 20 | 21 | ```sh 22 | yarn add eslint-plugin-react-native-unistyles -D 23 | ``` 24 | 25 | ## Configuration 26 | 27 | Add `plugins` section and specify react-native-unistyles as a plugin. 28 | 29 | ```json 30 | { 31 | "plugins": ["react-native-unistyles"] 32 | } 33 | ``` 34 | 35 | If it is not already the case you must also configure `ESLint` to support JSX. 36 | 37 | ```json 38 | { 39 | "parserOptions": { 40 | "ecmaFeatures": { 41 | "jsx": true 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | Then, enable all of the rules that you would like to use. 48 | 49 | ```json 50 | { 51 | "rules": { 52 | "react-native-unistyles/no-unused-styles": "warn", 53 | "react-native-unistyles/sort-styles": [ 54 | "warn", 55 | "asc", 56 | { "ignoreClassNames": false, "ignoreStyleProperties": false } 57 | ], 58 | } 59 | } 60 | ``` 61 | 62 | ## List of supported rules 63 | 64 | - [no-unused-styles](docs/rules/no-unused-styles.md): Detect `createStyleSheet` styles which are not used in your React components 65 | - [sort-styles](docs/rules/sort-styles.md): Detect `createStyleSheet` styles which are not in correct sort order 66 | 67 | ## Shareable configurations 68 | 69 | ### All 70 | 71 | This plugin also exports an `all` configuration that includes every available rule. 72 | 73 | ```js 74 | { 75 | "plugins": [ 76 | /* ... */ 77 | "react-native-unistyles" 78 | ], 79 | "extends": [/* ... */, "plugin:react-native-unistyles/all"] 80 | } 81 | ``` 82 | 83 | **Note**: These configurations will import `eslint-plugin-react-native-unistyles` and enable JSX in [parser options](http://eslint.org/docs/user-guide/configuring#specifying-parser-options). 84 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodSarhan/eslint-plugin-react-native-unistyles/b990443796fae5633cf6fe7a1fa7b421c5f5d0ba/assets/banner.png -------------------------------------------------------------------------------- /docs/rules/no-unused-styles.md: -------------------------------------------------------------------------------- 1 | # Detect unused Unistyles styles in React components 2 | 3 | When working on a component over a longer period of time, you could end up with unused unistyles styles that you forgot to delete. 4 | 5 | ## Rule Details 6 | 7 | The following patterns are considered warnings: 8 | 9 | ```js 10 | const styleSheet = createStyleSheet({ 11 | text: {} 12 | }); 13 | 14 | const MyComponent = () => { 15 | const {styles} = useStyles(styleSheet); 16 | return Hello 17 | }; 18 | ``` 19 | 20 | The following patterns are not considered warnings: 21 | 22 | ```js 23 | const styleSheet = createStyleSheet({ 24 | text: {} 25 | }); 26 | 27 | const MyComponent = () => { 28 | const {styles} = useStyles(styleSheet); 29 | return Hello 30 | }; 31 | ``` 32 | 33 | ```js 34 | const styleSheet = createStyleSheet({ 35 | text: {} 36 | }); 37 | 38 | const MyComponent = () => { 39 | const {styles: myStyles} = useStyles(styleSheet); 40 | return Hello 41 | }; 42 | ``` 43 | 44 | For using this rule with wrappers such as memo or forwarRef you can do the following 45 | 46 | ```js 47 | const MyComponent = () => {}; 48 | export const MyMemoizedComponent = memo(MyComponent); 49 | ``` 50 | 51 | instead of 52 | 53 | ```js 54 | export const MyComponent = memo(() => {}); 55 | ``` 56 | 57 | Styles referenced in a Style arrays are marked as used. 58 | 59 | Styles referenced in a conditional and logical expressions are marked as used. 60 | 61 | Style are also marked as used when they are used in tags that contain the word `style`. 62 | 63 | There should be at least one component in the file for this rule to take effect. 64 | -------------------------------------------------------------------------------- /docs/rules/sort-styles.md: -------------------------------------------------------------------------------- 1 | # Require createStyleSheet keys to be sorted 2 | It's like [sort-keys](https://eslint.org/docs/rules/sort-keys), but just for react-native-unistyles. 3 | 4 | Keeping your style definitions sorted is a common convention that helps with readability. This rule lets you enforce an ascending (default) or descending alphabetical order for both "class names" and style properties. 5 | 6 | ## Rule Details 7 | 8 | The following patterns are considered warnings: 9 | 10 | ```js 11 | const styles = StyleSheet.create({ 12 | button: { 13 | width: 100, 14 | color: 'green', 15 | }, 16 | }); 17 | ``` 18 | 19 | ```js 20 | const styles = StyleSheet.create({ 21 | button: {}, 22 | anchor: {}, 23 | }); 24 | ``` 25 | 26 | The following patterns are not considered warnings: 27 | 28 | ```js 29 | const styles = StyleSheet.create({ 30 | button: { 31 | color: 'green', 32 | width: 100, 33 | }, 34 | }); 35 | ``` 36 | 37 | ```js 38 | const styles = StyleSheet.create({ 39 | anchor: {}, 40 | button: {}, 41 | }); 42 | ``` 43 | 44 | ## Options 45 | 46 | ``` 47 | { 48 | "react-native-unistyles/sort-styles": ["error", "asc", { "ignoreClassNames": false, "ignoreStyleProperties": false }] 49 | } 50 | ``` 51 | 52 | The 1st option is "asc" or "desc". 53 | 54 | * `"asc"` (default) - enforce properties to be in ascending order. 55 | * `"desc"` - enforce properties to be in descending order. 56 | 57 | The 2nd option is an object which has 2 properties. 58 | 59 | * `ignoreClassNames` - if `true`, order will not be enforced on the class name level. Default is `false`. 60 | * `ignoreStyleProperties` - if `true`, order will not be enforced on the style property level. Default is `false`. 61 | 62 | ### desc 63 | 64 | `/* eslint react-native-unistyles/sort-styles: ["error", "desc"] */` 65 | 66 | The following patterns are considered warnings: 67 | 68 | ```js 69 | const styles = StyleSheet.create({ 70 | button: { 71 | color: 'green', 72 | width: 100, 73 | }, 74 | }); 75 | ``` 76 | 77 | ```js 78 | const styles = StyleSheet.create({ 79 | anchor: {}, 80 | button: {}, 81 | }); 82 | ``` 83 | 84 | The following patterns are not considered warnings: 85 | 86 | ```js 87 | const styles = StyleSheet.create({ 88 | button: { 89 | width: 100, 90 | color: 'green', 91 | }, 92 | }); 93 | ``` 94 | 95 | ```js 96 | const styles = StyleSheet.create({ 97 | button: {}, 98 | anchor: {}, 99 | }); 100 | ``` 101 | 102 | ### ignoreClassNames 103 | 104 | `/* eslint react-native-unistyles/sort-styles: ["error", "asc", { "ignoreClassNames": true }] */` 105 | 106 | The following patterns are not considered warnings: 107 | 108 | ```js 109 | const styles = StyleSheet.create({ 110 | button: { 111 | color: 'green', 112 | width: 100, 113 | }, 114 | anchor: {}, 115 | }); 116 | ``` 117 | 118 | # ignoreStyleProperties 119 | 120 | `/* eslint react-native-unistyles/sort-styles: ["error", "asc", { "ignoreStyleProperties": true }] */` 121 | 122 | The following patterns are not considered warnings: 123 | 124 | ```js 125 | const styles = StyleSheet.create({ 126 | anchor: {}, 127 | button: { 128 | width: 100, 129 | color: 'green', 130 | }, 131 | }); 132 | ``` 133 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | 'use strict'; 4 | 5 | const allRules = { 6 | 'no-unused-styles': require('./lib/rules/no-unused-styles'), 7 | 'sort-styles': require('./lib/rules/sort-styles'), 8 | }; 9 | 10 | function configureAsError(rules) { 11 | const result = {}; 12 | for (const key in rules) { 13 | if (!rules.hasOwnProperty(key)) { 14 | continue; 15 | } 16 | result['react-native-unistyles/' + key] = 2; 17 | } 18 | return result; 19 | } 20 | 21 | const allRulesConfig = configureAsError(allRules); 22 | 23 | module.exports = { 24 | deprecatedRules: {}, 25 | rules: allRules, 26 | rulesConfig: { 27 | 'no-unused-styles': 0, 28 | 'sort-styles': 0, 29 | }, 30 | configs: { 31 | all: { 32 | plugins: [ 33 | 'react-native-unistyles', 34 | ], 35 | parserOptions: { 36 | ecmaFeatures: { 37 | jsx: true, 38 | }, 39 | }, 40 | rules: allRulesConfig, 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /lib/rules/no-unused-styles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Detects unused styles 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const Components = require('../util/Components'); 8 | const styleSheet = require('../util/stylesheet'); 9 | 10 | const { StyleSheets } = styleSheet; 11 | const { astHelpers } = styleSheet; 12 | 13 | const create = Components.detect((context, components) => { 14 | const styleSheets = new StyleSheets(); 15 | const styleReferences = new Set(); 16 | 17 | function reportUnusedStyles(styleSheetsWithUnusedStyles) { 18 | Object.entries(styleSheetsWithUnusedStyles).forEach(([key, value]) => { 19 | const unusedStyles = value.properties; 20 | unusedStyles.forEach((node) => { 21 | const message = [ 22 | 'Unused style detected: ', 23 | key, 24 | '.', 25 | node.key.name, 26 | ].join(''); 27 | 28 | context.report(node, message); 29 | }); 30 | }); 31 | } 32 | 33 | return { 34 | VariableDeclaration: function (node) { 35 | if (astHelpers.isUseStylesHook(node)) { 36 | const destructuredStyleSheetName = astHelpers.getDestructuredStyleSheetName(node); 37 | const relatedStyleSheetObjectName = astHelpers.getRelatedStyleSheetObjectName(node); 38 | const parentComponentName = astHelpers.getParentComponentName(node); 39 | 40 | styleSheets.add( 41 | relatedStyleSheetObjectName, 42 | [ 43 | { 44 | nameInComponent: destructuredStyleSheetName, 45 | componentName: parentComponentName, 46 | }, 47 | ], 48 | [] 49 | ); 50 | } 51 | }, 52 | 53 | MemberExpression: function (node) { 54 | const styleRef = astHelpers.getPotentialStyleReferenceFromMemberExpression(node); 55 | if (styleRef) { 56 | styleReferences.add(styleRef); 57 | } 58 | }, 59 | 60 | CallExpression: function (node) { 61 | if (astHelpers.isStyleSheetDeclaration(node, context.settings)) { 62 | // TODO 63 | // const isExported = astHelpers.isStyleSheetExported(node); 64 | // if (isExported) { 65 | // return; 66 | // } 67 | const styleSheetObjectName = astHelpers.getStyleSheetObjectName(node); 68 | const styles = astHelpers.getStyleDeclarations(node); 69 | 70 | styleSheets.add(styleSheetObjectName, [], styles); 71 | } 72 | }, 73 | 74 | 'Program:exit': function () { 75 | const list = components.all(); 76 | if (Object.keys(list).length > 0) { 77 | styleReferences.forEach((reference) => { 78 | styleSheets.markAsUsed(reference); 79 | }); 80 | reportUnusedStyles(styleSheets.getUnusedReferences()); 81 | } 82 | }, 83 | }; 84 | }); 85 | 86 | module.exports = { 87 | meta: { 88 | schema: [], 89 | }, 90 | create, 91 | }; 92 | -------------------------------------------------------------------------------- /lib/rules/sort-styles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Rule to require StyleSheet object keys to be sorted 3 | */ 4 | 5 | 'use strict'; 6 | 7 | //------------------------------------------------------------------------------ 8 | // Requirements 9 | //------------------------------------------------------------------------------ 10 | 11 | const { astHelpers } = require('../util/stylesheet'); 12 | 13 | const { 14 | getStyleDeclarationsChunks, 15 | getPropertiesChunks, 16 | getStylePropertyIdentifier, 17 | isStyleSheetDeclaration, 18 | isEitherShortHand, 19 | } = astHelpers; 20 | 21 | //------------------------------------------------------------------------------ 22 | // Rule Definition 23 | //------------------------------------------------------------------------------ 24 | 25 | function create(context) { 26 | const order = context.options[0] || 'asc'; 27 | const options = context.options[1] || {}; 28 | const { ignoreClassNames } = options; 29 | const { ignoreStyleProperties } = options; 30 | const isValidOrder = order === 'asc' ? (a, b) => a <= b : (a, b) => a >= b; 31 | 32 | const sourceCode = context.getSourceCode(); 33 | 34 | function sort(array) { 35 | return [].concat(array).sort((a, b) => { 36 | const identifierA = getStylePropertyIdentifier(a); 37 | const identifierB = getStylePropertyIdentifier(b); 38 | 39 | let sortOrder = 0; 40 | if (isEitherShortHand(identifierA, identifierB)) { 41 | return a.range[0] - b.range[0]; 42 | } 43 | if (identifierA < identifierB) { 44 | sortOrder = -1; 45 | } else if (identifierA > identifierB) { 46 | sortOrder = 1; 47 | } 48 | return sortOrder * (order === 'asc' ? 1 : -1); 49 | }); 50 | } 51 | 52 | function report(array, type, node, prev, current) { 53 | const currentName = getStylePropertyIdentifier(current); 54 | const prevName = getStylePropertyIdentifier(prev); 55 | const hasComments = array 56 | .map((prop) => [ 57 | ...sourceCode.getCommentsBefore(prop), 58 | ...sourceCode.getCommentsAfter(prop), 59 | ]) 60 | .reduce((hasComment, comment) => hasComment || comment.length > 0, false); 61 | 62 | context.report({ 63 | node, 64 | message: `Expected ${type} to be in ${order}ending order. '${currentName}' should be before '${prevName}'.`, 65 | loc: current.key.loc, 66 | fix: hasComments 67 | ? undefined 68 | : (fixer) => { 69 | const sortedArray = sort(array); 70 | return array 71 | .map((item, i) => { 72 | if (item !== sortedArray[i]) { 73 | return fixer.replaceText(item, sourceCode.getText(sortedArray[i])); 74 | } 75 | return null; 76 | }) 77 | .filter(Boolean); 78 | }, 79 | }); 80 | } 81 | 82 | function checkIsSorted(array, arrayName, node) { 83 | for (let i = 1; i < array.length; i += 1) { 84 | const previous = array[i - 1]; 85 | const current = array[i]; 86 | 87 | if (previous.type !== 'Property' || current.type !== 'Property') { 88 | return; 89 | } 90 | 91 | const prevName = getStylePropertyIdentifier(previous); 92 | const currentName = getStylePropertyIdentifier(current); 93 | 94 | const oneIsShorthandForTheOther = arrayName === 'style properties' && isEitherShortHand(prevName, currentName); 95 | 96 | if (!oneIsShorthandForTheOther && !isValidOrder(prevName, currentName)) { 97 | return report(array, arrayName, node, previous, current); 98 | } 99 | } 100 | } 101 | 102 | return { 103 | CallExpression: function (node) { 104 | if (!isStyleSheetDeclaration(node, context.settings)) { 105 | return; 106 | } 107 | 108 | const classDefinitionsChunks = getStyleDeclarationsChunks(node); 109 | 110 | if (!ignoreClassNames) { 111 | classDefinitionsChunks.forEach((classDefinitions) => { 112 | checkIsSorted(classDefinitions, 'class names', node); 113 | }); 114 | } 115 | 116 | if (ignoreStyleProperties) return; 117 | 118 | classDefinitionsChunks.forEach((classDefinitions) => { 119 | classDefinitions.forEach((classDefinition) => { 120 | const styleProperties = classDefinition.value.properties; 121 | if (!styleProperties || styleProperties.length < 2) { 122 | return; 123 | } 124 | const stylePropertyChunks = getPropertiesChunks(styleProperties); 125 | stylePropertyChunks.forEach((stylePropertyChunk) => { 126 | checkIsSorted(stylePropertyChunk, 'style properties', node); 127 | }); 128 | }); 129 | }); 130 | }, 131 | }; 132 | } 133 | 134 | module.exports = { 135 | meta: { 136 | fixable: 'code', 137 | schema: [ 138 | { 139 | enum: ['asc', 'desc'], 140 | }, 141 | { 142 | type: 'object', 143 | properties: { 144 | ignoreClassNames: { 145 | type: 'boolean', 146 | }, 147 | ignoreStyleProperties: { 148 | type: 'boolean', 149 | }, 150 | }, 151 | additionalProperties: false, 152 | }, 153 | ], 154 | }, 155 | create, 156 | }; 157 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import eslint from 'eslint'; 2 | import estree from 'estree'; 3 | 4 | declare global { 5 | interface ASTNode extends estree.BaseNode { 6 | [_: string]: any; // TODO: fixme 7 | } 8 | type Scope = eslint.Scope.Scope; 9 | type Token = eslint.AST.Token; 10 | type Fixer = eslint.Rule.RuleFixer; 11 | type JSXAttribute = ASTNode; 12 | type JSXElement = ASTNode; 13 | type JSXFragment = ASTNode; 14 | type JSXOpeningElement = ASTNode; 15 | type JSXSpreadAttribute = ASTNode; 16 | 17 | type Context = eslint.Rule.RuleContext; 18 | 19 | type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set) => object; 20 | 21 | type TypeDeclarationBuilders = { 22 | [k in string]: TypeDeclarationBuilder; 23 | }; 24 | 25 | type UnionTypeDefinition = { 26 | type: 'union' | 'shape'; 27 | children: unknown[]; 28 | }; 29 | } -------------------------------------------------------------------------------- /lib/util/Components.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Utility class and functions for React components detection 3 | * @author Yannick Croissant 4 | */ 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * Components 10 | * @class 11 | */ 12 | function Components() { 13 | this.list = {}; 14 | this.getId = function (node) { 15 | return node && node.range.join(':'); 16 | }; 17 | } 18 | 19 | /** 20 | * Add a node to the components list, or update it if it's already in the list 21 | * 22 | * @param {ASTNode} node The AST node being added. 23 | * @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes) 24 | */ 25 | Components.prototype.add = function (node, confidence) { 26 | const id = this.getId(node); 27 | if (this.list[id]) { 28 | if (confidence === 0 || this.list[id].confidence === 0) { 29 | this.list[id].confidence = 0; 30 | } else { 31 | this.list[id].confidence = Math.max(this.list[id].confidence, confidence); 32 | } 33 | return; 34 | } 35 | this.list[id] = { 36 | node: node, 37 | confidence: confidence, 38 | }; 39 | }; 40 | 41 | /** 42 | * Find a component in the list using its node 43 | * 44 | * @param {ASTNode} node The AST node being searched. 45 | * @returns {Object} Component object, undefined if the component is not found 46 | */ 47 | Components.prototype.get = function (node) { 48 | const id = this.getId(node); 49 | return this.list[id]; 50 | }; 51 | 52 | /** 53 | * Update a component in the list 54 | * 55 | * @param {ASTNode} node The AST node being updated. 56 | * @param {Object} props Additional properties to add to the component. 57 | */ 58 | Components.prototype.set = function (node, props) { 59 | let currentNode = node; 60 | while (currentNode && !this.list[this.getId(currentNode)]) { 61 | currentNode = node.parent; 62 | } 63 | if (!currentNode) { 64 | return; 65 | } 66 | const id = this.getId(currentNode); 67 | this.list[id] = { ...this.list[id], ...props }; 68 | }; 69 | 70 | /** 71 | * Return the components list 72 | * Components for which we are not confident are not returned 73 | * 74 | * @returns {Object} Components list 75 | */ 76 | Components.prototype.all = function () { 77 | const list = {}; 78 | Object.keys(this.list).forEach((i) => { 79 | if ({}.hasOwnProperty.call(this.list, i) && this.list[i].confidence >= 2) { 80 | list[i] = this.list[i]; 81 | } 82 | }); 83 | return list; 84 | }; 85 | 86 | /** 87 | * Return the length of the components list 88 | * Components for which we are not confident are not counted 89 | * 90 | * @returns {Number} Components list length 91 | */ 92 | Components.prototype.length = function () { 93 | let length = 0; 94 | Object.keys(this.list).forEach((i) => { 95 | if ({}.hasOwnProperty.call(this.list, i) && this.list[i].confidence >= 2) { 96 | length += 1; 97 | } 98 | }); 99 | return length; 100 | }; 101 | 102 | function componentRule(rule, context) { 103 | const components = new Components(); 104 | 105 | // Utilities for component detection 106 | const utils = { 107 | 108 | /** 109 | * Check if the node is returning JSX 110 | * 111 | * @param {ASTNode} node The AST node being checked (must be a ReturnStatement). 112 | * @returns {Boolean} True if the node is returning JSX, false if not 113 | */ 114 | isReturningJSX: function (node) { 115 | let property; 116 | switch (node.type) { 117 | case 'ReturnStatement': 118 | property = 'argument'; 119 | break; 120 | case 'ArrowFunctionExpression': 121 | property = 'body'; 122 | break; 123 | default: 124 | return false; 125 | } 126 | 127 | const returnsJSX = node[property] 128 | && (node[property].type === 'JSXElement' || node[property].type === 'JSXFragment'); 129 | const returnsReactCreateElement = node[property] 130 | && node[property].callee 131 | && node[property].callee.property 132 | && node[property].callee.property.name === 'createElement'; 133 | return Boolean(returnsJSX || returnsReactCreateElement); 134 | }, 135 | 136 | /** 137 | * Get the parent component node from the current scope 138 | * 139 | * @returns {ASTNode} component node, null if we are not in a component 140 | */ 141 | getParentComponent: function () { 142 | return ( 143 | utils.getParentStatelessComponent() 144 | ); 145 | }, 146 | 147 | /** 148 | * Get the parent stateless component node from the current scope 149 | * 150 | * @returns {ASTNode} component node, null if we are not in a component 151 | */ 152 | getParentStatelessComponent: function () { 153 | // eslint-disable-next-line react/destructuring-assignment 154 | let scope = context.getScope(); 155 | while (scope) { 156 | const node = scope.block; 157 | // Ignore non functions 158 | const isFunction = /Function/.test(node.type); 159 | // Ignore classes methods 160 | const isNotMethod = !node.parent || node.parent.type !== 'MethodDefinition'; 161 | // Ignore arguments (callback, etc.) 162 | const isNotArgument = !node.parent || node.parent.type !== 'CallExpression'; 163 | if (isFunction && isNotMethod && isNotArgument) { 164 | return node; 165 | } 166 | scope = scope.upper; 167 | } 168 | return null; 169 | }, 170 | 171 | /** 172 | * Get the related component from a node 173 | * 174 | * @param {ASTNode} node The AST node being checked (must be a MemberExpression). 175 | * @returns {ASTNode} component node, null if we cannot find the component 176 | */ 177 | getRelatedComponent: function (node) { 178 | let currentNode = node; 179 | let i; 180 | let j; 181 | let k; 182 | let l; 183 | // Get the component path 184 | const componentPath = []; 185 | while (currentNode) { 186 | if (currentNode.property && currentNode.property.type === 'Identifier') { 187 | componentPath.push(currentNode.property.name); 188 | } 189 | if (currentNode.object && currentNode.object.type === 'Identifier') { 190 | componentPath.push(currentNode.object.name); 191 | } 192 | currentNode = currentNode.object; 193 | } 194 | componentPath.reverse(); 195 | 196 | // Find the variable in the current scope 197 | const variableName = componentPath.shift(); 198 | if (!variableName) { 199 | return null; 200 | } 201 | let variableInScope; 202 | const { variables } = context.getScope(); 203 | for (i = 0, j = variables.length; i < j; i++) { // eslint-disable-line no-plusplus 204 | if (variables[i].name === variableName) { 205 | variableInScope = variables[i]; 206 | break; 207 | } 208 | } 209 | if (!variableInScope) { 210 | return null; 211 | } 212 | 213 | // Find the variable declaration 214 | let defInScope; 215 | const { defs } = variableInScope; 216 | for (i = 0, j = defs.length; i < j; i++) { // eslint-disable-line no-plusplus 217 | if ( 218 | defs[i].type === 'ClassName' 219 | || defs[i].type === 'FunctionName' 220 | || defs[i].type === 'Variable' 221 | ) { 222 | defInScope = defs[i]; 223 | break; 224 | } 225 | } 226 | if (!defInScope) { 227 | return null; 228 | } 229 | currentNode = defInScope.node.init || defInScope.node; 230 | 231 | // Traverse the node properties to the component declaration 232 | for (i = 0, j = componentPath.length; i < j; i++) { // eslint-disable-line no-plusplus 233 | if (!currentNode.properties) { 234 | continue; // eslint-disable-line no-continue 235 | } 236 | for (k = 0, l = currentNode.properties.length; k < l; k++) { // eslint-disable-line no-plusplus, max-len 237 | if (currentNode.properties[k].key.name === componentPath[i]) { 238 | currentNode = currentNode.properties[k]; 239 | break; 240 | } 241 | } 242 | if (!currentNode) { 243 | return null; 244 | } 245 | currentNode = currentNode.value; 246 | } 247 | 248 | // Return the component 249 | return components.get(currentNode); 250 | }, 251 | }; 252 | 253 | // Component detection instructions 254 | const detectionInstructions = { 255 | 256 | FunctionExpression: function () { 257 | const node = utils.getParentComponent(); 258 | if (!node) { 259 | return; 260 | } 261 | components.add(node, 1); 262 | }, 263 | 264 | FunctionDeclaration: function () { 265 | const node = utils.getParentComponent(); 266 | if (!node) { 267 | return; 268 | } 269 | components.add(node, 1); 270 | }, 271 | 272 | ArrowFunctionExpression: function () { 273 | const node = utils.getParentComponent(); 274 | if (!node) { 275 | return; 276 | } 277 | if (node.expression && utils.isReturningJSX(node)) { 278 | components.add(node, 2); 279 | } else { 280 | components.add(node, 1); 281 | } 282 | }, 283 | 284 | ThisExpression: function () { 285 | const node = utils.getParentComponent(); 286 | if (!node || !/Function/.test(node.type)) { 287 | return; 288 | } 289 | // Ban functions with a ThisExpression 290 | components.add(node, 0); 291 | }, 292 | 293 | ReturnStatement: function (node) { 294 | if (!utils.isReturningJSX(node)) { 295 | return; 296 | } 297 | const parentNode = utils.getParentComponent(); 298 | if (!parentNode) { 299 | return; 300 | } 301 | components.add(parentNode, 2); 302 | }, 303 | }; 304 | 305 | // Update the provided rule instructions to add the component detection 306 | const ruleInstructions = rule(context, components, utils); 307 | const updatedRuleInstructions = { ...ruleInstructions }; 308 | Object.keys(detectionInstructions).forEach((instruction) => { 309 | updatedRuleInstructions[instruction] = (node) => { 310 | detectionInstructions[instruction](node); 311 | return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : undefined; 312 | }; 313 | }); 314 | // Return the updated rule instructions 315 | return updatedRuleInstructions; 316 | } 317 | 318 | Components.detect = function (rule) { 319 | return componentRule.bind(this, rule); 320 | }; 321 | 322 | module.exports = Components; 323 | -------------------------------------------------------------------------------- /lib/util/stylesheet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * StyleSheets represents the StyleSheets found in the source code. 5 | * @constructor 6 | */ 7 | function StyleSheets() { 8 | this.styleSheets = {}; 9 | } 10 | 11 | /** 12 | * Add adds a StyleSheet to our StyleSheets collections. 13 | * 14 | * @param {string} styleSheetName - The name of the StyleSheet. 15 | * @param {object} properties - The collection of rules in the styleSheet. 16 | */ 17 | StyleSheets.prototype.add = function ( 18 | styleSheetName, 19 | occurrences, 20 | properties 21 | ) { 22 | if (!this.styleSheets[styleSheetName]) { 23 | this.styleSheets[styleSheetName] = { 24 | properties, 25 | occurrences: [...occurrences], 26 | }; 27 | } else { 28 | this.styleSheets[styleSheetName].properties = [ 29 | ...this.styleSheets[styleSheetName].properties, 30 | ...properties, 31 | ]; 32 | this.styleSheets[styleSheetName].occurrences = [ 33 | ...this.styleSheets[styleSheetName].occurrences, 34 | ...occurrences, 35 | ]; 36 | } 37 | }; 38 | 39 | /** 40 | * MarkAsUsed marks a rule as used in our source code by removing it from the 41 | * specified StyleSheet rules. 42 | * 43 | * @param {string} fullyQualifiedName - The fully qualified name of the rule. 44 | * for example 'styles.text' 45 | */ 46 | StyleSheets.prototype.markAsUsed = function (fullyQualifiedName) { 47 | const nameSplit = fullyQualifiedName.split('.'); 48 | const parentComponentName = nameSplit[0]; 49 | const styleSheetNameFromStyle = nameSplit[1]; 50 | const styleSheetPropertyFromStyle = nameSplit[2]; 51 | 52 | const relatedStyleSheetEntry = Object.entries(this.styleSheets).find( 53 | (entry) => { 54 | const sheet = entry[1]; 55 | const sheetOccurrences = sheet.occurrences; 56 | const matchingOccurence = sheetOccurrences.find( 57 | (occurrence) => occurrence.componentName === parentComponentName 58 | && occurrence.nameInComponent === styleSheetNameFromStyle 59 | ); 60 | return matchingOccurence; 61 | } 62 | ); 63 | 64 | if (relatedStyleSheetEntry) { 65 | const relatedStyleSheetkey = relatedStyleSheetEntry[0]; 66 | const newProperties = this.styleSheets[relatedStyleSheetkey].properties.filter( 67 | (property) => property.key.name !== styleSheetPropertyFromStyle 68 | ); 69 | this.styleSheets[relatedStyleSheetkey].properties = newProperties; 70 | } 71 | }; 72 | 73 | /** 74 | * GetUnusedReferences returns all collected StyleSheets and their 75 | * unmarked rules. 76 | */ 77 | StyleSheets.prototype.getUnusedReferences = function () { 78 | const unusedReferences = {}; 79 | Object.entries(this.styleSheets).forEach(([key, value]) => { 80 | if (value.properties.length > 0) { 81 | unusedReferences[key] = value; 82 | } 83 | }); 84 | 85 | return unusedReferences; 86 | }; 87 | 88 | // let currentContent; 89 | // const getSourceCode = (node) => currentContent 90 | // .getSourceCode(node) 91 | // .getText(node); 92 | 93 | // const getSomethingFromSettings = 94 | // (settings) => settings['react-native-unistyles/something-setting']; 95 | 96 | const astHelpers = { 97 | 98 | containsCreateStyleSheetCall: function (node) { 99 | return Boolean( 100 | node 101 | && node.type === 'CallExpression' 102 | && node.callee 103 | && node.callee.name === 'createStyleSheet' 104 | ); 105 | }, 106 | 107 | isStyleSheetDeclaration: function (node) { 108 | return Boolean( 109 | astHelpers.containsCreateStyleSheetCall(node) 110 | ); 111 | }, 112 | 113 | // TODO check if the stylesheet is exported, and add "ignore-exported- sheets" setting 114 | // isStyleSheetExported: function (node) { 115 | // return false; 116 | // }, 117 | 118 | isUseStylesHook: function (node) { 119 | return Boolean( 120 | node 121 | && node.type === 'VariableDeclaration' 122 | && node.declarations 123 | && node.declarations[0] 124 | && node.declarations[0].init 125 | && node.declarations[0].init.callee 126 | && node.declarations[0].init.callee.name === 'useStyles' 127 | ); 128 | }, 129 | 130 | getDestructuredStyleSheetName: function (node) { 131 | if ( 132 | node 133 | && node.declarations 134 | && node.declarations[0] 135 | && node.declarations[0].id 136 | && node.declarations[0].id.type === 'ObjectPattern' 137 | ) { 138 | const destructuringObject = node.declarations[0].id; 139 | const stylesObject = destructuringObject.properties.find((property) => property.key.name === 'styles'); 140 | if (stylesObject && stylesObject.value.name) { 141 | return stylesObject.value.name; 142 | } 143 | } 144 | }, 145 | 146 | getParentComponentName: (node) => { 147 | if (!node.parent) { 148 | return undefined; 149 | } 150 | if (node.parent && node.parent.type === 'Program') { 151 | const componentNode = node; 152 | if (componentNode.type === 'VariableDeclaration') { 153 | return componentNode.declarations[0].id.name; 154 | } 155 | if (componentNode.type === 'FunctionDeclaration') { 156 | return componentNode.id.name; 157 | } 158 | if (componentNode.type === 'ExportNamedDeclaration' || componentNode.type === 'ExportDefaultDeclaration') { 159 | if (componentNode.declaration.type === 'FunctionDeclaration') { 160 | return componentNode.declaration.id.name; 161 | } 162 | if (componentNode.declaration.type === 'VariableDeclaration') { 163 | return componentNode.declaration.declarations[0].id.name; 164 | } 165 | } 166 | return undefined; 167 | } 168 | return astHelpers.getParentComponentName(node.parent); 169 | }, 170 | 171 | getRelatedStyleSheetObjectName: function (node) { 172 | if ( 173 | node 174 | && node.declarations 175 | && node.declarations[0] 176 | && node.declarations[0].init 177 | && node.declarations[0].init 178 | && node.declarations[0].init.arguments 179 | && node.declarations[0].init.arguments[0] 180 | && node.declarations[0].init.arguments[0].name 181 | ) { 182 | return node.declarations[0].init.arguments[0].name; 183 | } 184 | }, 185 | 186 | getStyleSheetObjectName: function (node) { 187 | if (node && node.parent && node.parent.id) { 188 | return node.parent.id.name; 189 | } 190 | }, 191 | 192 | getStyleDeclarations: function (node) { 193 | if ( 194 | node 195 | && node.type === 'CallExpression' 196 | && node.arguments 197 | && node.arguments[0] 198 | && node.arguments[0].properties 199 | ) { 200 | return node.arguments[0].properties.filter((property) => property.type === 'Property'); 201 | } 202 | 203 | if ( 204 | node 205 | && node.type === 'CallExpression' 206 | && node.arguments 207 | && node.arguments[0] 208 | && node.arguments[0].type === 'ArrowFunctionExpression' 209 | && node.arguments[0].body 210 | && node.arguments[0].body.properties 211 | ) { 212 | return node.arguments[0].body.properties.filter((property) => property.type === 'Property'); 213 | } 214 | 215 | if ( 216 | node 217 | && node.type === 'CallExpression' 218 | && node.arguments 219 | && node.arguments[0] 220 | && node.arguments[0].type === 'ArrowFunctionExpression' 221 | && node.arguments[0].body 222 | && node.arguments[0].body.body 223 | ) { 224 | const bodies = node.arguments[0].body.body; 225 | const indexOfReturnStatement = bodies.findIndex((body) => body.type === 'ReturnStatement'); 226 | if ( 227 | indexOfReturnStatement !== -1 228 | && bodies[indexOfReturnStatement].argument 229 | && bodies[indexOfReturnStatement].argument.properties 230 | ) { 231 | return bodies[indexOfReturnStatement].argument.properties.filter((property) => property.type === 'Property'); 232 | } 233 | } 234 | 235 | if ( 236 | node 237 | && node.type === 'CallExpression' 238 | && node.arguments 239 | && node.arguments[0] 240 | && node.arguments[0].type === 'FunctionExpression' 241 | && node.arguments[0].body 242 | && node.arguments[0].body.body 243 | ) { 244 | const bodies = node.arguments[0].body.body; 245 | const indexOfReturnStatement = bodies.findIndex((body) => body.type === 'ReturnStatement'); 246 | if ( 247 | indexOfReturnStatement !== -1 248 | && bodies[indexOfReturnStatement].argument 249 | && bodies[indexOfReturnStatement].argument.properties 250 | ) { 251 | return bodies[indexOfReturnStatement].argument.properties.filter((property) => property.type === 'Property'); 252 | } 253 | } 254 | 255 | return []; 256 | }, 257 | 258 | getPotentialStyleReferenceFromMemberExpression: function (node) { 259 | if ( 260 | node 261 | && node.object 262 | && node.object.type === 'Identifier' 263 | && node.object.name 264 | && node.property 265 | && node.property.type === 'Identifier' 266 | && node.property.name 267 | && node.parent.type !== 'MemberExpression' 268 | ) { 269 | const parentComponentName = astHelpers.getParentComponentName(node); 270 | if (parentComponentName) { 271 | return [parentComponentName, node.object.name, node.property.name].join('.'); 272 | } 273 | } 274 | }, 275 | 276 | getStyleDeclarationsChunks: function (node) { 277 | const getChunks = (properties) => { 278 | const result = []; 279 | let chunk = []; 280 | for (let i = 0; i < properties.length; i += 1) { 281 | const property = properties[i]; 282 | if (property.type === 'Property') { 283 | chunk.push(property); 284 | } else if (chunk.length) { 285 | result.push(chunk); 286 | chunk = []; 287 | } 288 | } 289 | if (chunk.length) { 290 | result.push(chunk); 291 | } 292 | return result; 293 | }; 294 | 295 | if ( 296 | node 297 | && node.type === 'CallExpression' 298 | && node.arguments 299 | && node.arguments[0] 300 | ) { 301 | if (node.arguments[0].properties) { 302 | return getChunks(node.arguments[0].properties); 303 | } 304 | 305 | if (node.arguments[0].type === 'ArrowFunctionExpression') { 306 | if (node.arguments[0].body && node.arguments[0].body.properties) { 307 | return getChunks(node.arguments[0].body.properties); 308 | } 309 | 310 | if (node.arguments[0].body && node.arguments[0].body.body) { 311 | const bodies = node.arguments[0].body.body; 312 | const indexOfReturnStatement = bodies.findIndex( 313 | (body) => body.type === 'ReturnStatement' 314 | ); 315 | if ( 316 | indexOfReturnStatement !== -1 317 | && bodies[indexOfReturnStatement].argument 318 | && bodies[indexOfReturnStatement].argument.properties 319 | ) { 320 | return getChunks(bodies[indexOfReturnStatement].argument.properties); 321 | } 322 | } 323 | } 324 | 325 | if (node.arguments[0].type === 'FunctionExpression' && node.arguments[0].body && node.arguments[0].body.body) { 326 | const bodies = node.arguments[0].body.body; 327 | const indexOfReturnStatement = bodies.findIndex( 328 | (body) => body.type === 'ReturnStatement' 329 | ); 330 | if ( 331 | indexOfReturnStatement !== -1 332 | && bodies[indexOfReturnStatement].argument 333 | && bodies[indexOfReturnStatement].argument.properties 334 | ) { 335 | return getChunks(bodies[indexOfReturnStatement].argument.properties); 336 | } 337 | } 338 | } 339 | 340 | return []; 341 | }, 342 | 343 | getPropertiesChunks: function (properties) { 344 | const result = []; 345 | let chunk = []; 346 | for (let i = 0; i < properties.length; i += 1) { 347 | const property = properties[i]; 348 | if (property.type === 'Property') { 349 | chunk.push(property); 350 | } else if (chunk.length) { 351 | result.push(chunk); 352 | chunk = []; 353 | } 354 | } 355 | if (chunk.length) { 356 | result.push(chunk); 357 | } 358 | return result; 359 | }, 360 | 361 | getExpressionIdentifier: function (node) { 362 | if (node) { 363 | switch (node.type) { 364 | case 'Identifier': 365 | return node.name; 366 | case 'Literal': 367 | return node.value; 368 | case 'TemplateLiteral': 369 | return node.quasis.reduce( 370 | (result, quasi, index) => result 371 | + quasi.value.cooked 372 | + astHelpers.getExpressionIdentifier(node.expressions[index]), 373 | '' 374 | ); 375 | default: 376 | return ''; 377 | } 378 | } 379 | 380 | return ''; 381 | }, 382 | 383 | getStylePropertyIdentifier: function (node) { 384 | if ( 385 | node 386 | && node.key 387 | ) { 388 | return astHelpers.getExpressionIdentifier(node.key); 389 | } 390 | }, 391 | 392 | isEitherShortHand: function (property1, property2) { 393 | const shorthands = ['margin', 'padding', 'border', 'flex']; 394 | if (shorthands.includes(property1)) { 395 | return property2.startsWith(property1); 396 | } if (shorthands.includes(property2)) { 397 | return property1.startsWith(property2); 398 | } 399 | return false; 400 | }, 401 | }; 402 | 403 | module.exports.astHelpers = astHelpers; 404 | module.exports.StyleSheets = StyleSheets; 405 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-react-native-unistyles", 3 | "version": "0.2.9", 4 | "author": "RodSarhan", 5 | "license": "MIT", 6 | "description": "React Native Unistyles rules for ESLint", 7 | "main": "index.js", 8 | "scripts": { 9 | "lint": "eslint ./lib && eslint ./tests", 10 | "test": "npm run lint && npm run unit-test", 11 | "unit-test": "nyc --silent --reporter=text mocha tests/**/*.js", 12 | "test-one-case": "nyc --silent --reporter=text mocha tests/lib/rules/test-one-case.js" 13 | }, 14 | "files": [ 15 | "LICENSE", 16 | "README.md", 17 | "index.js", 18 | "lib" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/RodSarhan/eslint-plugin-react-native-unistyles" 23 | }, 24 | "homepage": "https://github.com/RodSarhan/eslint-plugin-react-native-unistyles", 25 | "bugs": "https://github.com/RodSarhan/eslint-plugin-react-native-unistyles/issues", 26 | "devDependencies": { 27 | "@babel/eslint-parser": "^7.16.3", 28 | "@typescript-eslint/parser": "^6.6.0", 29 | "eslint": "^8.4.0", 30 | "eslint-config-airbnb": "^19.0.2", 31 | "eslint-plugin-import": "^2.25.2", 32 | "eslint-plugin-jsx-a11y": "^6.4.1", 33 | "eslint-plugin-react": "^7.26.1", 34 | "mocha": "^10.2.0", 35 | "nyc": "^15.1.0", 36 | "typescript": "^5.2.2" 37 | }, 38 | "peerDependencies": { 39 | "eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8" 40 | }, 41 | "keywords": [ 42 | "eslint", 43 | "eslint-plugin", 44 | "eslintplugin", 45 | "react", 46 | "react-native", 47 | "react native", 48 | "unistyles", 49 | "react-native-unistyles" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint-disable global-require */ 3 | 4 | 'use strict'; 5 | 6 | const assert = require('assert'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const plugin = require('..'); 10 | 11 | const rules = fs.readdirSync(path.resolve(__dirname, '../lib/rules/')) 12 | .map((f) => path.basename(f, '.js')); 13 | 14 | const defaultSettings = { 15 | 'jsx-uses-vars': 1, 16 | }; 17 | 18 | describe('all rule files should be exported by the plugin', () => { 19 | rules.forEach((ruleName) => { 20 | it('should export ' + ruleName, () => { 21 | assert.equal( 22 | plugin.rules[ruleName], 23 | require(path.join('../lib/rules', ruleName)) // eslint-disable-line import/no-dynamic-require 24 | ); 25 | }); 26 | 27 | if ({}.hasOwnProperty.call(defaultSettings, ruleName)) { 28 | const val = defaultSettings[ruleName]; 29 | it('should configure ' + ruleName + ' to ' + val + ' by default', () => { 30 | assert.equal( 31 | plugin.rulesConfig[ruleName], 32 | val 33 | ); 34 | }); 35 | } else { 36 | it('should configure ' + ruleName + ' off by default', () => { 37 | assert.equal( 38 | plugin.rulesConfig[ruleName], 39 | 0 40 | ); 41 | }); 42 | } 43 | }); 44 | }); 45 | 46 | describe('configurations', () => { 47 | it('should export a \'all\' configuration', () => { 48 | assert(plugin.configs.all); 49 | Object.keys(plugin.configs.all.rules).forEach((configName) => { 50 | assert.equal(configName.indexOf('react-native-unistyles/'), 0); 51 | assert.equal(plugin.configs.all.rules[configName], 2); 52 | }); 53 | rules.forEach((ruleName) => { 54 | const inDeprecatedRules = Boolean(plugin.deprecatedRules[ruleName]); 55 | const inAllConfig = Boolean(plugin.configs.all.rules['react-native-unistyles/' + ruleName]); 56 | assert(inDeprecatedRules ^ inAllConfig); // eslint-disable-line no-bitwise 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/lib/rules/no-unused-styles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview No unused styles defined in javascript files 3 | */ 4 | 5 | 'use strict'; 6 | 7 | // ------------------------------------------------------------------------------ 8 | // Requirements 9 | // ------------------------------------------------------------------------------ 10 | 11 | const { RuleTester } = require('eslint'); 12 | const rule = require('../../../lib/rules/no-unused-styles'); 13 | 14 | require('@babel/eslint-parser'); 15 | 16 | // ------------------------------------------------------------------------------ 17 | // Tests 18 | // ------------------------------------------------------------------------------ 19 | 20 | const ruleTester = new RuleTester(); 21 | const tests = { 22 | valid: [{ 23 | code: ` 24 | const MyComponent = () => { 25 | const {styles} = useStyles(styleSheet); 26 | return ( 27 | Hello 28 | ); 29 | }; 30 | const styleSheet = createStyleSheet({ 31 | name: {}, 32 | }); 33 | `, 34 | }, { 35 | code: ` 36 | const MyComponent = () => { 37 | const {styles} = useStyles(styleSheet); 38 | return Hello; 39 | }; 40 | const styleSheet = createStyleSheet((theme) => ({ 41 | name: {}, 42 | })); 43 | `, 44 | }, { 45 | code: ` 46 | const MyComponent = () => { 47 | const {styles} = useStyles(styleSheet); 48 | return Hello; 49 | }; 50 | const styleSheet = createStyleSheet((theme) => { 51 | return { 52 | name: {}, 53 | }; 54 | }); 55 | `, 56 | }, { 57 | code: ` 58 | const MyComponent = () => { 59 | const {styles} = useStyles(styleSheet); 60 | return Hello; 61 | }; 62 | const styleSheet = createStyleSheet((theme) => { 63 | const someVar = 'name'; 64 | return { 65 | name: {}, 66 | }; 67 | }); 68 | `, 69 | }, { 70 | code: ` 71 | const styleSheet = createStyleSheet({ 72 | text: {} 73 | }); 74 | 75 | const MyComponent = () => { 76 | const {styles: myStyles} = useStyles(styleSheet); 77 | return Hello 78 | }; 79 | `, 80 | }, { 81 | code: ` 82 | const styleSheet = createStyleSheet((theme) => ({ 83 | text: {}, 84 | viewStyle: {} 85 | })); 86 | 87 | const MyComponent = () => { 88 | const {theme, styles: myStyles} = useStyles(styleSheet); 89 | return Hello 90 | }; 91 | `, 92 | }, { 93 | code: ` 94 | const styleSheet1 = createStyleSheet((theme) => ({ 95 | text: {}, 96 | })); 97 | 98 | const styleSheet2 = createStyleSheet((theme) => ({ 99 | viewStyle: {} 100 | })); 101 | 102 | const MyComponent = () => { 103 | const {theme, styles: myStyles1} = useStyles(styleSheet1); 104 | const textStyle = myStyles1.text 105 | return Hello 106 | }; 107 | const MyComponent2 = () => { 108 | const {theme, styles: myStyles2} = useStyles(styleSheet2); 109 | return 110 | }; 111 | `, 112 | }, 113 | { 114 | code: ` 115 | const MyComponent = () => { 116 | const {styles} = useStyles(styleSheet); 117 | return Hello; 118 | }; 119 | const styleSheet = createStyleSheet(function returnStyles(theme) { 120 | return { 121 | name: {}, 122 | }; 123 | }); 124 | `, 125 | }, { 126 | code: ` 127 | const MyComponent = () => { 128 | const {styles} = useStyles(styleSheet); 129 | return Hello; 130 | }; 131 | const styleSheet = createStyleSheet(function returnStyles(theme) { 132 | const someVar = 'name'; 133 | return { 134 | name: {}, 135 | }; 136 | }); 137 | `, 138 | }, { 139 | code: ` 140 | const styleSheet = createStyleSheet({ 141 | name: {}, 142 | }); 143 | const MyComponent = () => { 144 | const {styles} = useStyles(styleSheet); 145 | return ( 146 | Hello 147 | ); 148 | }; 149 | `, 150 | }, { 151 | code: ` 152 | const styleSheet = createStyleSheet({ 153 | name: {}, 154 | welcome: {}, 155 | }); 156 | const MyComponent = () => { 157 | const {styles} = useStyles(styleSheet); 158 | return ( 159 | Hello 160 | ); 161 | }; 162 | const MyOtherComponent = () => { 163 | const {styles} = useStyles(styleSheet); 164 | return ( 165 | Hello 166 | ); 167 | }; 168 | `, 169 | }, { 170 | code: ` 171 | const styleSheet = createStyleSheet({ 172 | text: {}, 173 | }); 174 | const MyComponent = () => { 175 | const {styles} = useStyles(styleSheet); 176 | return ( 177 | Hello 178 | ); 179 | }; 180 | `, 181 | }, { 182 | code: ` 183 | const styleSheet = createStyleSheet({ 184 | text: {}, 185 | }); 186 | const MyComponent = () => { 187 | const {styles} = useStyles(styleSheet); 188 | const condition1 = true; 189 | const condition2 = true; 190 | 191 | return ( 192 | Hello 193 | ); 194 | }; 195 | `, 196 | }, { 197 | code: ` 198 | const styleSheet = createStyleSheet({ 199 | text1: {}, 200 | text2: {}, 201 | }); 202 | const MyComponent = () => { 203 | const {styles} = useStyles(styleSheet); 204 | const condition = true; 205 | 206 | return ( 207 | Hello 208 | ); 209 | }; 210 | `, 211 | }, { 212 | code: ` 213 | const styleSheet = createStyleSheet({ 214 | style1: { 215 | color: 'red', 216 | }, 217 | style2: { 218 | color: 'blue', 219 | }, 220 | }); 221 | export const MyComponent = ({isRed}) => { 222 | const {styles} = useStyles(styleSheet); 223 | 224 | return ( 225 | Hello 226 | ); 227 | }; 228 | `, 229 | }, { 230 | code: ` 231 | const styleSheet1 = createStyleSheet((theme) => ({someStyle1: {}})); 232 | const styleSheet2 = createStyleSheet((theme) => ({someStyle1: {}})); 233 | 234 | const MyComponent1 = () => { 235 | const {styles} = useStyles(styleSheet1); 236 | return ; 237 | }; 238 | 239 | const MyComponent2 = () => { 240 | const {styles} = useStyles(styleSheet2); 241 | return ; 242 | }; 243 | `, 244 | }, { 245 | code: ` 246 | const styleSheet1 = createStyleSheet((theme) => ({someStyle1: {}})); 247 | const styleSheet2 = createStyleSheet((theme) => ({someStyle2: {}})); 248 | 249 | const MyComponent1 = () => { 250 | const {styles} = useStyles(styleSheet1); 251 | return ; 252 | }; 253 | 254 | const MyComponent2 = () => { 255 | const {styles} = useStyles(styleSheet2); 256 | return ; 257 | }; 258 | `, 259 | }, { 260 | code: ` 261 | const styleSheet = createStyleSheet({ 262 | style1: { 263 | color: 'red', 264 | }, 265 | style2: { 266 | color: 'blue', 267 | }, 268 | }); 269 | export function MyComponent ({isRed}) { 270 | const {styles} = useStyles(styleSheet); 271 | 272 | return ( 273 | Hello 274 | ); 275 | }; 276 | `, 277 | }, { 278 | code: ` 279 | const styleSheet = createStyleSheet({ 280 | name: {}, 281 | }); 282 | `, 283 | }, { 284 | code: ` 285 | const styleSheet = createStyleSheet({}); 286 | const MyComponent = () => { 287 | const {styles} = useStyles(styleSheet); 288 | return ( 289 | Hello 290 | ); 291 | } 292 | `, 293 | }, { 294 | code: ` 295 | const MyComponent = () => { 296 | const {styles} = useStyles(styleSheet); 297 | const condition = true; 298 | const myStyle = condition ? styles.text1 : styles.text2; 299 | 300 | return ( 301 | Hello 302 | ); 303 | }; 304 | const styleSheet = createStyleSheet({ 305 | text1: {}, 306 | text2: {}, 307 | }); 308 | `, 309 | }, { 310 | code: ` 311 | const additionalStyles = {}; 312 | const styleSheet = createStyleSheet({ 313 | text: {}, 314 | ...additionalStyles, 315 | }); 316 | const MyComponent = () => { 317 | const {styles} = useStyles(styleSheet); 318 | 319 | return ( 320 | Hello 321 | ); 322 | }; 323 | `, 324 | }, { 325 | code: ` 326 | const styleSheet = createStyleSheet({ 327 | text: {}, 328 | }); 329 | export default function MyComponent() { 330 | const {styles} = useStyles(styleSheet); 331 | 332 | return ( 333 | Hello 334 | ); 335 | }; 336 | `, 337 | }], 338 | 339 | invalid: [{ 340 | code: ` 341 | const styleSheet = createStyleSheet({ 342 | text: {}, 343 | }); 344 | const MyComponent = () => { 345 | const {styles} = useStyles(styleSheet); 346 | return ( 347 | Hello 348 | ); 349 | }; 350 | `, 351 | errors: [{ 352 | message: 'Unused style detected: styleSheet.text', 353 | }], 354 | }, { 355 | code: ` 356 | const styleSheet = createStyleSheet(() => { 357 | return { 358 | text: {}, 359 | }; 360 | }); 361 | const MyComponent = () => { 362 | const {styles} = useStyles(styleSheet); 363 | return ( 364 | Hello 365 | ); 366 | }; 367 | `, 368 | errors: [{ 369 | message: 'Unused style detected: styleSheet.text', 370 | }], 371 | }, { 372 | code: ` 373 | const styleSheet = createStyleSheet(() => { 374 | return { 375 | text: {}, 376 | other: {}, 377 | }; 378 | }); 379 | const MyComponent = () => { 380 | const {styles: myStyles} = useStyles(styleSheet); 381 | return ( 382 | Hello 383 | ); 384 | }; 385 | `, 386 | errors: [{ 387 | message: 'Unused style detected: styleSheet.text', 388 | }], 389 | }, { 390 | code: ` 391 | const styleSheet = createStyleSheet((theme) => ({ 392 | container: {}, 393 | })); 394 | const MyComponent = () => { 395 | const {styles: myStyles} = useStyles(styleSheet); 396 | return ( 397 | 398 | ); 399 | }; 400 | `, 401 | errors: [{ 402 | message: 'Unused style detected: styleSheet.container', 403 | }], 404 | }, { 405 | code: ` 406 | const styleSheet = createStyleSheet(() => { 407 | return { 408 | text: {}, 409 | other: {}, 410 | }; 411 | }); 412 | const MyComponent = () => { 413 | const {styles: myStyles} = useStyles(styleSheet); 414 | return ( 415 | Hello 416 | ); 417 | }; 418 | `, 419 | errors: [{ 420 | message: 'Unused style detected: styleSheet.text', 421 | }, { 422 | message: 'Unused style detected: styleSheet.other', 423 | }], 424 | }, { 425 | code: ` 426 | export const styleSheet = createStyleSheet(() => ({ 427 | foo: {}, 428 | bar: {}, 429 | })); 430 | export const MyComponent = () => { 431 | const {styles} = useStyles(styleSheet); 432 | return ( 433 | Hello 434 | ); 435 | }; 436 | `, 437 | errors: [{ 438 | message: 'Unused style detected: styleSheet.bar', 439 | }], 440 | }, 441 | { 442 | code: ` 443 | export const MyComponent = wrapper(() => { 444 | const {styles} = useStyles(styleSheet); 445 | return ( 446 | Hello 447 | ); 448 | }); 449 | export const styleSheet = createStyleSheet(() => ({ 450 | foo: {}, 451 | bar: {}, 452 | })); 453 | `, 454 | errors: [{ 455 | message: 'Unused style detected: styleSheet.bar', 456 | }], 457 | }, 458 | { 459 | code: ` 460 | const MyComponent = () => { 461 | const {styles} = useStyles(styleSheet); 462 | return ( 463 | Hello 464 | ); 465 | }; 466 | 467 | export const WrappedComponent = wrapper(MyComponent); 468 | 469 | export const styleSheet = createStyleSheet(() => ({ 470 | foo: {}, 471 | bar: {}, 472 | })); 473 | `, 474 | errors: [{ 475 | message: 'Unused style detected: styleSheet.bar', 476 | }], 477 | }, 478 | ], 479 | }; 480 | 481 | const config = { 482 | parser: require.resolve('@babel/eslint-parser'), 483 | parserOptions: { 484 | requireConfigFile: false, 485 | babelOptions: { 486 | parserOpts: { 487 | plugins: [ 488 | ['estree', { classFeatures: true }], 489 | 'jsx', 490 | ], 491 | }, 492 | }, 493 | }, 494 | settings: {}, 495 | }; 496 | 497 | tests.valid.forEach((t) => Object.assign(t, config)); 498 | tests.invalid.forEach((t) => Object.assign(t, config)); 499 | 500 | ruleTester.run('no-unused-styles', rule, tests); 501 | -------------------------------------------------------------------------------- /tests/lib/rules/test-one-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview No unused styles defined in javascript files 3 | */ 4 | 5 | 'use strict'; 6 | 7 | // ------------------------------------------------------------------------------ 8 | // Requirements 9 | // ------------------------------------------------------------------------------ 10 | 11 | const { RuleTester } = require('eslint'); 12 | const rule = require('../../../lib/rules/no-unused-styles'); 13 | 14 | require('@babel/eslint-parser'); 15 | 16 | // ------------------------------------------------------------------------------ 17 | // Tests 18 | // ------------------------------------------------------------------------------ 19 | 20 | const ruleTester = new RuleTester(); 21 | const tests = { 22 | valid: [{ 23 | code: ` 24 | const styleSheet = createStyleSheet({ 25 | style1: { 26 | color: 'red', 27 | }, 28 | style2: { 29 | color: 'blue', 30 | }, 31 | }); 32 | export function MyComponent ({isRed}) { 33 | const {styles} = useStyles(styleSheet); 34 | 35 | return ( 36 | Hello 37 | ); 38 | }; 39 | `, 40 | }], 41 | 42 | invalid: [], 43 | }; 44 | 45 | const config = { 46 | parser: require.resolve('@babel/eslint-parser'), 47 | parserOptions: { 48 | requireConfigFile: false, 49 | babelOptions: { 50 | parserOpts: { 51 | plugins: [ 52 | ['estree', { classFeatures: true }], 53 | 'jsx', 54 | ], 55 | }, 56 | }, 57 | }, 58 | settings: {}, 59 | }; 60 | 61 | tests.valid.forEach((t) => Object.assign(t, config)); 62 | tests.invalid.forEach((t) => Object.assign(t, config)); 63 | 64 | ruleTester.run('no-unused-styles', rule, tests); 65 | --------------------------------------------------------------------------------