├── .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 |
2 |
3 | # ESLint plugin for React Native Unistyles
4 |
5 |  [](https://github.com/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 |
--------------------------------------------------------------------------------