oxlint-plugin-react-native 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/dist/index.js +19 -0
- package/dist/rules/no-color-literals.js +49 -0
- package/dist/rules/no-inline-styles.js +39 -0
- package/dist/rules/no-raw-text.js +108 -0
- package/dist/rules/no-single-element-style-arrays.js +41 -0
- package/dist/rules/no-unused-styles.js +58 -0
- package/dist/rules/sort-styles.js +122 -0
- package/dist/util/Components.js +284 -0
- package/dist/util/stylesheet.js +365 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# oxlint-plugin-react-native
|
|
2
|
+
|
|
3
|
+
Lint rules for [React Native](https://reactnative.dev/) projects, built for [Oxlint](https://github.com/oxc-project/oxc).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
**npm**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install oxlint-plugin-react-native oxlint --save-dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**yarn**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yarn add -D oxlint-plugin-react-native oxlint
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
In your Oxlint config (e.g. `.oxlintrc.json`), register the plugin by **name** and enable the rules you want:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"jsPlugins": [
|
|
30
|
+
{ "name": "react-native", "specifier": "oxlint-plugin-react-native" }
|
|
31
|
+
],
|
|
32
|
+
"rules": {
|
|
33
|
+
"react-native/no-color-literals": "error",
|
|
34
|
+
"react-native/no-inline-styles": "warn",
|
|
35
|
+
"react-native/no-raw-text": "error",
|
|
36
|
+
"react-native/no-single-element-style-arrays": "error",
|
|
37
|
+
"react-native/no-unused-styles": "warn",
|
|
38
|
+
"react-native/sort-styles": "warn"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Rules
|
|
46
|
+
|
|
47
|
+
| Rule | Description | Fix |
|
|
48
|
+
| ------------------------------------------------------------------------ | --------------------------------------------------------- | :-: |
|
|
49
|
+
| [no-color-literals](docs/no-color-literals.md) | Disallow color literals in styles; use variables or theme | — |
|
|
50
|
+
| [no-inline-styles](docs/no-inline-styles.md) | Disallow inline style objects; prefer `StyleSheet.create` | — |
|
|
51
|
+
| [no-raw-text](docs/no-raw-text.md) | Require text inside `<Text>` (or allowed components) | — |
|
|
52
|
+
| [no-single-element-style-arrays](docs/no-single-element-style-arrays.md) | Disallow single-element style arrays (`style={[x]}`) | ✅ |
|
|
53
|
+
| [no-unused-styles](docs/no-unused-styles.md) | Report unused styles from `StyleSheet.create` | — |
|
|
54
|
+
| [sort-styles](docs/sort-styles.md) | Enforce order of class names and style properties | ✅ |
|
|
55
|
+
|
|
56
|
+
Each rule is documented in the [docs](docs/) folder with examples and options.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Scripts
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
| ---------------- | ----------------------------- |
|
|
64
|
+
| `npm run build` | Compile TypeScript to `dist/` |
|
|
65
|
+
| `npm run lint` | Run Oxlint |
|
|
66
|
+
| `npm run format` | Run Oxlint with `--fix` |
|
|
67
|
+
| `npm test` | Run tests |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { eslintCompatPlugin } from '@oxlint/plugins';
|
|
2
|
+
import noUnusedStyles from './rules/no-unused-styles.js';
|
|
3
|
+
import noInlineStyles from './rules/no-inline-styles.js';
|
|
4
|
+
import noColorLiterals from './rules/no-color-literals.js';
|
|
5
|
+
import sortStyles from './rules/sort-styles.js';
|
|
6
|
+
import noRawText from './rules/no-raw-text.js';
|
|
7
|
+
import noSingleElementStyleArrays from './rules/no-single-element-style-arrays.js';
|
|
8
|
+
const allRules = {
|
|
9
|
+
'no-unused-styles': noUnusedStyles,
|
|
10
|
+
'no-inline-styles': noInlineStyles,
|
|
11
|
+
'no-color-literals': noColorLiterals,
|
|
12
|
+
'sort-styles': sortStyles,
|
|
13
|
+
'no-raw-text': noRawText,
|
|
14
|
+
'no-single-element-style-arrays': noSingleElementStyleArrays,
|
|
15
|
+
};
|
|
16
|
+
export default eslintCompatPlugin({
|
|
17
|
+
meta: { name: 'oxlint-plugin-react-native' },
|
|
18
|
+
rules: allRules,
|
|
19
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { detect } from '../util/Components.js';
|
|
2
|
+
import { StyleSheets, astHelpers } from '../util/stylesheet.js';
|
|
3
|
+
import * as util from 'util';
|
|
4
|
+
const rule = detect((context) => {
|
|
5
|
+
let styleSheets;
|
|
6
|
+
return {
|
|
7
|
+
before() {
|
|
8
|
+
styleSheets = new StyleSheets();
|
|
9
|
+
},
|
|
10
|
+
CallExpression: (node) => {
|
|
11
|
+
if (astHelpers.isStyleSheetDeclaration(node, context.settings)) {
|
|
12
|
+
const styles = astHelpers.getStyleDeclarations(node);
|
|
13
|
+
if (styles) {
|
|
14
|
+
styles.forEach((style) => {
|
|
15
|
+
const literals = astHelpers.collectColorLiterals(style.value, context);
|
|
16
|
+
styleSheets.addColorLiterals(literals);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
JSXAttribute: (node) => {
|
|
22
|
+
if (astHelpers.isStyleAttribute(node)) {
|
|
23
|
+
const literals = astHelpers.collectColorLiterals(node.value, context);
|
|
24
|
+
styleSheets.addColorLiterals(literals);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
'Program:exit': () => {
|
|
28
|
+
const colorLiterals = styleSheets.getColorLiterals();
|
|
29
|
+
if (colorLiterals) {
|
|
30
|
+
colorLiterals.forEach((style) => {
|
|
31
|
+
if (style) {
|
|
32
|
+
const expression = util.inspect(style.expression);
|
|
33
|
+
context.report({
|
|
34
|
+
node: style.node,
|
|
35
|
+
message: 'Color literal: {{expression}}',
|
|
36
|
+
data: { expression },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
export default {
|
|
45
|
+
meta: {
|
|
46
|
+
schema: [],
|
|
47
|
+
},
|
|
48
|
+
createOnce: rule
|
|
49
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { detect } from '../util/Components.js';
|
|
2
|
+
import { StyleSheets, astHelpers } from '../util/stylesheet.js';
|
|
3
|
+
import * as util from 'util';
|
|
4
|
+
const rule = detect((context) => {
|
|
5
|
+
// Setup state per-file (createOnce)
|
|
6
|
+
let styleSheets;
|
|
7
|
+
return {
|
|
8
|
+
before() {
|
|
9
|
+
styleSheets = new StyleSheets();
|
|
10
|
+
},
|
|
11
|
+
JSXAttribute: (node) => {
|
|
12
|
+
if (astHelpers.isStyleAttribute(node)) {
|
|
13
|
+
const styles = astHelpers.collectStyleObjectExpressions(node.value, context);
|
|
14
|
+
styleSheets.addObjectExpressions(styles);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
'Program:exit': () => {
|
|
18
|
+
const inlineStyles = styleSheets.getObjectExpressions();
|
|
19
|
+
if (inlineStyles) {
|
|
20
|
+
inlineStyles.forEach((style) => {
|
|
21
|
+
if (style) {
|
|
22
|
+
const expression = util.inspect(style.expression);
|
|
23
|
+
context.report({
|
|
24
|
+
node: style.node,
|
|
25
|
+
message: 'Inline style: {{expression}}',
|
|
26
|
+
data: { expression },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
export default {
|
|
35
|
+
meta: {
|
|
36
|
+
schema: [],
|
|
37
|
+
},
|
|
38
|
+
createOnce: rule
|
|
39
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const elementName = (node) => {
|
|
2
|
+
const reversedIdentifiers = [];
|
|
3
|
+
if (node.type === 'JSXElement' &&
|
|
4
|
+
node.openingElement.type === 'JSXOpeningElement') {
|
|
5
|
+
let object = node.openingElement.name;
|
|
6
|
+
while (object.type === 'JSXMemberExpression') {
|
|
7
|
+
if (object.property.type === 'JSXIdentifier') {
|
|
8
|
+
reversedIdentifiers.push(object.property.name);
|
|
9
|
+
}
|
|
10
|
+
object = object.object;
|
|
11
|
+
}
|
|
12
|
+
if (object.type === 'JSXIdentifier') {
|
|
13
|
+
reversedIdentifiers.push(object.name);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return reversedIdentifiers.reverse().join('.');
|
|
17
|
+
};
|
|
18
|
+
const hasAllowedParent = (parent, allowedElements) => {
|
|
19
|
+
let curNode = parent;
|
|
20
|
+
while (curNode) {
|
|
21
|
+
if (curNode.type === 'JSXElement') {
|
|
22
|
+
const name = elementName(curNode);
|
|
23
|
+
if (allowedElements.includes(name)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
curNode = curNode.parent;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
};
|
|
31
|
+
const rule = (context) => {
|
|
32
|
+
// Defer context.options to visitor (oxlint forbids in createOnce).
|
|
33
|
+
let _allowedElements;
|
|
34
|
+
function getAllowedElements() {
|
|
35
|
+
if (_allowedElements === undefined) {
|
|
36
|
+
const options = context.options[0] || {};
|
|
37
|
+
const skippedElements = options.skip ? options.skip : [];
|
|
38
|
+
_allowedElements = [
|
|
39
|
+
'Text',
|
|
40
|
+
'TSpan',
|
|
41
|
+
'StyledText',
|
|
42
|
+
'Animated.Text',
|
|
43
|
+
].concat(skippedElements);
|
|
44
|
+
}
|
|
45
|
+
return _allowedElements;
|
|
46
|
+
}
|
|
47
|
+
const report = (node) => {
|
|
48
|
+
const errorValue = node.type === 'TemplateLiteral'
|
|
49
|
+
? `TemplateLiteral: ${node.expressions[0].name}`
|
|
50
|
+
: node.value.trim();
|
|
51
|
+
const formattedErrorValue = errorValue.length > 0 ? `Raw text (${errorValue})` : 'Whitespace(s)';
|
|
52
|
+
context.report({
|
|
53
|
+
node,
|
|
54
|
+
message: `${formattedErrorValue} cannot be used outside of a <Text> tag`,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const hasOnlyLineBreak = (value) => /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ''));
|
|
58
|
+
const getValidation = (node) => !hasAllowedParent(node.parent, getAllowedElements());
|
|
59
|
+
return {
|
|
60
|
+
Literal(node) {
|
|
61
|
+
const parentType = node.parent.type;
|
|
62
|
+
const onlyFor = ['JSXExpressionContainer', 'JSXElement'];
|
|
63
|
+
if (typeof node.value !== 'string' ||
|
|
64
|
+
hasOnlyLineBreak(node.value) ||
|
|
65
|
+
!onlyFor.includes(parentType) ||
|
|
66
|
+
(node.parent.parent && node.parent.parent.type === 'JSXAttribute'))
|
|
67
|
+
return;
|
|
68
|
+
const isStringLiteral = parentType === 'JSXExpressionContainer';
|
|
69
|
+
if (getValidation(isStringLiteral ? node.parent : node)) {
|
|
70
|
+
report(node);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
JSXText(node) {
|
|
74
|
+
if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value))
|
|
75
|
+
return;
|
|
76
|
+
if (getValidation(node)) {
|
|
77
|
+
report(node);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
TemplateLiteral(node) {
|
|
81
|
+
if (node.parent.type !== 'JSXExpressionContainer' ||
|
|
82
|
+
(node.parent.parent && node.parent.parent.type === 'JSXAttribute'))
|
|
83
|
+
return;
|
|
84
|
+
if (getValidation(node.parent)) {
|
|
85
|
+
report(node);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
export default {
|
|
91
|
+
meta: {
|
|
92
|
+
schema: [
|
|
93
|
+
{
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
skip: {
|
|
97
|
+
type: 'array',
|
|
98
|
+
items: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
createOnce: rule,
|
|
108
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const rule = (context) => {
|
|
2
|
+
function reportNode(JSXExpressionNode) {
|
|
3
|
+
context.report({
|
|
4
|
+
node: JSXExpressionNode,
|
|
5
|
+
message: 'Single element style arrays are not necessary and cause unnecessary re-renders',
|
|
6
|
+
fix(fixer) {
|
|
7
|
+
const realStyleNode = JSXExpressionNode.value.expression.elements[0];
|
|
8
|
+
const styleSource = context.sourceCode.getText(realStyleNode);
|
|
9
|
+
return fixer.replaceText(JSXExpressionNode.value.expression, styleSource);
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
// --------------------------------------------------------------------------
|
|
14
|
+
// Public
|
|
15
|
+
// --------------------------------------------------------------------------
|
|
16
|
+
return {
|
|
17
|
+
JSXAttribute(node) {
|
|
18
|
+
if (node.name.name !== 'style')
|
|
19
|
+
return;
|
|
20
|
+
if (!node.value.expression)
|
|
21
|
+
return;
|
|
22
|
+
if (node.value.expression.type !== 'ArrayExpression')
|
|
23
|
+
return;
|
|
24
|
+
if (node.value.expression.elements.length === 1) {
|
|
25
|
+
reportNode(node);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export default {
|
|
31
|
+
meta: {
|
|
32
|
+
docs: {
|
|
33
|
+
description: 'Disallow single element style arrays. These cause unnecessary re-renders as the identity of the array always changes',
|
|
34
|
+
category: 'Stylistic Issues',
|
|
35
|
+
recommended: false,
|
|
36
|
+
url: '',
|
|
37
|
+
},
|
|
38
|
+
fixable: 'code',
|
|
39
|
+
},
|
|
40
|
+
createOnce: rule,
|
|
41
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { detect } from '../util/Components.js';
|
|
2
|
+
import { StyleSheets, astHelpers } from '../util/stylesheet.js';
|
|
3
|
+
const rule = detect((context, components) => {
|
|
4
|
+
let styleSheets;
|
|
5
|
+
let styleReferences;
|
|
6
|
+
function reportUnusedStyles(unusedStyles) {
|
|
7
|
+
Object.keys(unusedStyles).forEach((key) => {
|
|
8
|
+
if ({}.hasOwnProperty.call(unusedStyles, key)) {
|
|
9
|
+
const styles = unusedStyles[key];
|
|
10
|
+
styles.forEach((node) => {
|
|
11
|
+
const message = [
|
|
12
|
+
'Unused style detected: ',
|
|
13
|
+
key,
|
|
14
|
+
'.',
|
|
15
|
+
node.key.name,
|
|
16
|
+
].join('');
|
|
17
|
+
context.report({ node, message });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
before() {
|
|
24
|
+
styleSheets = new StyleSheets();
|
|
25
|
+
styleReferences = new Set();
|
|
26
|
+
},
|
|
27
|
+
MemberExpression: function (node) {
|
|
28
|
+
const styleRef = astHelpers.getPotentialStyleReferenceFromMemberExpression(node);
|
|
29
|
+
if (styleRef) {
|
|
30
|
+
styleReferences.add(styleRef);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
CallExpression: function (node) {
|
|
34
|
+
if (astHelpers.isStyleSheetDeclaration(node, context.settings)) {
|
|
35
|
+
const styleSheetName = astHelpers.getStyleSheetName(node);
|
|
36
|
+
const styles = astHelpers.getStyleDeclarations(node);
|
|
37
|
+
if (styleSheetName) {
|
|
38
|
+
styleSheets.add(styleSheetName, styles);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
'Program:exit': function () {
|
|
43
|
+
const list = components.all();
|
|
44
|
+
if (Object.keys(list).length > 0) {
|
|
45
|
+
styleReferences.forEach((reference) => {
|
|
46
|
+
styleSheets.markAsUsed(reference);
|
|
47
|
+
});
|
|
48
|
+
reportUnusedStyles(styleSheets.getUnusedReferences());
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
export default {
|
|
54
|
+
meta: {
|
|
55
|
+
schema: [],
|
|
56
|
+
},
|
|
57
|
+
createOnce: rule,
|
|
58
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { astHelpers } from '../util/stylesheet.js';
|
|
2
|
+
const rule = (context) => {
|
|
3
|
+
// Defer context.options and context.sourceCode to visitor (oxlint forbids in createOnce).
|
|
4
|
+
function sort(array, order) {
|
|
5
|
+
return [...array].sort((a, b) => {
|
|
6
|
+
const identifierA = astHelpers.getStylePropertyIdentifier(a) || '';
|
|
7
|
+
const identifierB = astHelpers.getStylePropertyIdentifier(b) || '';
|
|
8
|
+
let sortOrder = 0;
|
|
9
|
+
if (astHelpers.isEitherShortHand(identifierA, identifierB)) {
|
|
10
|
+
return a.range[0] - b.range[0];
|
|
11
|
+
}
|
|
12
|
+
if (identifierA < identifierB) {
|
|
13
|
+
sortOrder = -1;
|
|
14
|
+
}
|
|
15
|
+
else if (identifierA > identifierB) {
|
|
16
|
+
sortOrder = 1;
|
|
17
|
+
}
|
|
18
|
+
return sortOrder * (order === 'asc' ? 1 : -1);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function report(array, type, node, prev, current, order, sourceCode) {
|
|
22
|
+
const currentName = astHelpers.getStylePropertyIdentifier(current);
|
|
23
|
+
const prevName = astHelpers.getStylePropertyIdentifier(prev);
|
|
24
|
+
const hasComments = array
|
|
25
|
+
.map((prop) => [
|
|
26
|
+
...sourceCode.getCommentsBefore(prop),
|
|
27
|
+
...sourceCode.getCommentsAfter(prop),
|
|
28
|
+
])
|
|
29
|
+
.reduce((hasComment, comment) => hasComment || comment.length > 0, false);
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
message: `Expected ${type} to be in ${order}ending order. '${currentName}' should be before '${prevName}'.`,
|
|
33
|
+
loc: current.key.loc,
|
|
34
|
+
fix: hasComments
|
|
35
|
+
? undefined
|
|
36
|
+
: (fixer) => {
|
|
37
|
+
const sortedArray = sort(array, order);
|
|
38
|
+
return array
|
|
39
|
+
.map((item, i) => {
|
|
40
|
+
if (item !== sortedArray[i]) {
|
|
41
|
+
return fixer.replaceText(item, sourceCode.getText(sortedArray[i]));
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
})
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function checkIsSorted(array, arrayName, node, order, options, sourceCode, isValidOrder) {
|
|
50
|
+
for (let i = 1; i < array.length; i += 1) {
|
|
51
|
+
const previous = array[i - 1];
|
|
52
|
+
const current = array[i];
|
|
53
|
+
if (previous.type !== 'Property' || current.type !== 'Property') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const prevName = astHelpers.getStylePropertyIdentifier(previous) || '';
|
|
57
|
+
const currentName = astHelpers.getStylePropertyIdentifier(current) || '';
|
|
58
|
+
const oneIsShorthandForTheOther = arrayName === 'style properties' &&
|
|
59
|
+
astHelpers.isEitherShortHand(prevName, currentName);
|
|
60
|
+
if (!oneIsShorthandForTheOther && !isValidOrder(prevName, currentName)) {
|
|
61
|
+
return report(array, arrayName, node, previous, current, order, sourceCode);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
CallExpression: function (node) {
|
|
67
|
+
const order = context.options[0] || 'asc';
|
|
68
|
+
const options = context.options[1] || {};
|
|
69
|
+
const { ignoreClassNames, ignoreStyleProperties } = options;
|
|
70
|
+
const sourceCode = context.sourceCode;
|
|
71
|
+
const isValidOrder = order === 'asc'
|
|
72
|
+
? (a, b) => a <= b
|
|
73
|
+
: (a, b) => a >= b;
|
|
74
|
+
if (!astHelpers.isStyleSheetDeclaration(node, context.settings)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const classDefinitionsChunks = astHelpers.getStyleDeclarationsChunks(node);
|
|
78
|
+
if (!ignoreClassNames) {
|
|
79
|
+
classDefinitionsChunks.forEach((classDefinitions) => {
|
|
80
|
+
checkIsSorted(classDefinitions, 'class names', node, order, options, sourceCode, isValidOrder);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (ignoreStyleProperties)
|
|
84
|
+
return;
|
|
85
|
+
classDefinitionsChunks.forEach((classDefinitions) => {
|
|
86
|
+
classDefinitions.forEach((classDefinition) => {
|
|
87
|
+
const styleProperties = classDefinition.value.properties;
|
|
88
|
+
if (!styleProperties || styleProperties.length < 2) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const stylePropertyChunks = astHelpers.getPropertiesChunks(styleProperties);
|
|
92
|
+
stylePropertyChunks.forEach((stylePropertyChunk) => {
|
|
93
|
+
checkIsSorted(stylePropertyChunk, 'style properties', node, order, options, sourceCode, isValidOrder);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
export default {
|
|
101
|
+
meta: {
|
|
102
|
+
fixable: 'code',
|
|
103
|
+
schema: [
|
|
104
|
+
{
|
|
105
|
+
enum: ['asc', 'desc'],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
ignoreClassNames: {
|
|
111
|
+
type: 'boolean',
|
|
112
|
+
},
|
|
113
|
+
ignoreStyleProperties: {
|
|
114
|
+
type: 'boolean',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
createOnce: rule,
|
|
122
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
export class Components {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.list = {};
|
|
4
|
+
}
|
|
5
|
+
getId(node) {
|
|
6
|
+
return node ? node.range.join(':') : '';
|
|
7
|
+
}
|
|
8
|
+
add(node, confidence) {
|
|
9
|
+
const id = this.getId(node);
|
|
10
|
+
if (this.list[id]) {
|
|
11
|
+
if (confidence === 0 || this.list[id].confidence === 0) {
|
|
12
|
+
this.list[id].confidence = 0;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
this.list[id].confidence = Math.max(this.list[id].confidence, confidence);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.list[id] = {
|
|
20
|
+
node: node,
|
|
21
|
+
confidence: confidence,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
get(node) {
|
|
25
|
+
const id = this.getId(node);
|
|
26
|
+
return this.list[id];
|
|
27
|
+
}
|
|
28
|
+
set(node, props) {
|
|
29
|
+
let currentNode = node;
|
|
30
|
+
while (currentNode && !this.list[this.getId(currentNode)]) {
|
|
31
|
+
currentNode = currentNode.parent;
|
|
32
|
+
}
|
|
33
|
+
if (!currentNode) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const id = this.getId(currentNode);
|
|
37
|
+
this.list[id] = { ...this.list[id], ...props };
|
|
38
|
+
}
|
|
39
|
+
all() {
|
|
40
|
+
const list = {};
|
|
41
|
+
Object.keys(this.list).forEach((i) => {
|
|
42
|
+
if (Object.prototype.hasOwnProperty.call(this.list, i) &&
|
|
43
|
+
this.list[i].confidence >= 2) {
|
|
44
|
+
list[i] = this.list[i];
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return list;
|
|
48
|
+
}
|
|
49
|
+
length() {
|
|
50
|
+
let length = 0;
|
|
51
|
+
Object.keys(this.list).forEach((i) => {
|
|
52
|
+
if (Object.prototype.hasOwnProperty.call(this.list, i) &&
|
|
53
|
+
this.list[i].confidence >= 2) {
|
|
54
|
+
length += 1;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return length;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function componentRule(rule, context) {
|
|
61
|
+
const components = new Components();
|
|
62
|
+
// Access context.sourceCode only inside utils (when called during traversal), not during createOnce.
|
|
63
|
+
const utils = {
|
|
64
|
+
isES5Component: function (node) {
|
|
65
|
+
if (!node.parent) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return /^(React\.)?createClass$/.test(context.sourceCode.getText(node.parent.callee));
|
|
69
|
+
},
|
|
70
|
+
isES6Component: function (node) {
|
|
71
|
+
if (!node.superClass) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return /^(React\.)?(Pure)?Component$/.test(context.sourceCode.getText(node.superClass));
|
|
75
|
+
},
|
|
76
|
+
isReturningJSX: function (node) {
|
|
77
|
+
let property;
|
|
78
|
+
switch (node.type) {
|
|
79
|
+
case 'ReturnStatement':
|
|
80
|
+
property = 'argument';
|
|
81
|
+
break;
|
|
82
|
+
case 'ArrowFunctionExpression':
|
|
83
|
+
property = 'body';
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const returnsJSX = node[property] &&
|
|
89
|
+
(node[property].type === 'JSXElement' ||
|
|
90
|
+
node[property].type === 'JSXFragment');
|
|
91
|
+
const returnsReactCreateElement = node[property] &&
|
|
92
|
+
node[property].callee &&
|
|
93
|
+
node[property].callee.property &&
|
|
94
|
+
node[property].callee.property.name === 'createElement';
|
|
95
|
+
return Boolean(returnsJSX || returnsReactCreateElement);
|
|
96
|
+
},
|
|
97
|
+
getParentComponent: function (_n) {
|
|
98
|
+
return (utils.getParentES6Component(_n) ||
|
|
99
|
+
utils.getParentES5Component(_n) ||
|
|
100
|
+
utils.getParentStatelessComponent(_n));
|
|
101
|
+
},
|
|
102
|
+
getParentES5Component: function (_n) {
|
|
103
|
+
let scope = (context.sourceCode || context).getScope(_n);
|
|
104
|
+
while (scope) {
|
|
105
|
+
const node = scope.block && scope.block.parent && scope.block.parent.parent;
|
|
106
|
+
if (node && utils.isES5Component(node)) {
|
|
107
|
+
return node;
|
|
108
|
+
}
|
|
109
|
+
scope = scope.upper;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
},
|
|
113
|
+
getParentES6Component: function (_n) {
|
|
114
|
+
let scope = (context.sourceCode || context).getScope(_n);
|
|
115
|
+
while (scope && scope.type !== 'class') {
|
|
116
|
+
scope = scope.upper;
|
|
117
|
+
}
|
|
118
|
+
const node = scope && scope.block;
|
|
119
|
+
if (!node || !utils.isES6Component(node)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return node;
|
|
123
|
+
},
|
|
124
|
+
getParentStatelessComponent: function (_n) {
|
|
125
|
+
let scope = (context.sourceCode || context).getScope(_n);
|
|
126
|
+
while (scope) {
|
|
127
|
+
const node = scope.block;
|
|
128
|
+
const isFunction = /Function/.test(node.type);
|
|
129
|
+
const isNotMethod = !node.parent || node.parent.type !== 'MethodDefinition';
|
|
130
|
+
const isNotArgument = !node.parent || node.parent.type !== 'CallExpression';
|
|
131
|
+
if (isFunction && isNotMethod && isNotArgument) {
|
|
132
|
+
return node;
|
|
133
|
+
}
|
|
134
|
+
scope = scope.upper;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
},
|
|
138
|
+
getRelatedComponent: function (node) {
|
|
139
|
+
let currentNode = node;
|
|
140
|
+
let i;
|
|
141
|
+
let j;
|
|
142
|
+
let k;
|
|
143
|
+
let l;
|
|
144
|
+
const componentPath = [];
|
|
145
|
+
while (currentNode) {
|
|
146
|
+
if (currentNode.property &&
|
|
147
|
+
currentNode.property.type === 'Identifier') {
|
|
148
|
+
componentPath.push(currentNode.property.name);
|
|
149
|
+
}
|
|
150
|
+
if (currentNode.object && currentNode.object.type === 'Identifier') {
|
|
151
|
+
componentPath.push(currentNode.object.name);
|
|
152
|
+
}
|
|
153
|
+
currentNode = currentNode.object;
|
|
154
|
+
}
|
|
155
|
+
componentPath.reverse();
|
|
156
|
+
const variableName = componentPath.shift();
|
|
157
|
+
if (!variableName) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
let variableInScope;
|
|
161
|
+
const { variables } = (context.sourceCode || context).getScope(node);
|
|
162
|
+
for (i = 0, j = variables.length; i < j; i++) {
|
|
163
|
+
if (variables[i].name === variableName) {
|
|
164
|
+
variableInScope = variables[i];
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!variableInScope) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
let defInScope;
|
|
172
|
+
const { defs } = variableInScope;
|
|
173
|
+
for (i = 0, j = defs.length; i < j; i++) {
|
|
174
|
+
if (defs[i].type === 'ClassName' ||
|
|
175
|
+
defs[i].type === 'FunctionName' ||
|
|
176
|
+
defs[i].type === 'Variable') {
|
|
177
|
+
defInScope = defs[i];
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!defInScope) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
currentNode = defInScope.node.init || defInScope.node;
|
|
185
|
+
for (i = 0, j = componentPath.length; i < j; i++) {
|
|
186
|
+
if (!currentNode.properties) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// @ts-ignore
|
|
190
|
+
for (k = 0, l = currentNode.properties.length; k < l; k++) {
|
|
191
|
+
if (currentNode.properties[k].key.name === componentPath[i]) {
|
|
192
|
+
currentNode = currentNode.properties[k];
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!currentNode) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
currentNode = currentNode.value;
|
|
200
|
+
}
|
|
201
|
+
return components.get(currentNode);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const detectionInstructions = {
|
|
205
|
+
ClassDeclaration: function (node) {
|
|
206
|
+
if (!utils.isES6Component(node)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
components.add(node, 2);
|
|
210
|
+
},
|
|
211
|
+
ClassProperty: function (_n) {
|
|
212
|
+
const node = utils.getParentComponent(_n);
|
|
213
|
+
if (!node) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
components.add(node, 2);
|
|
217
|
+
},
|
|
218
|
+
ObjectExpression: function (node) {
|
|
219
|
+
if (!utils.isES5Component(node)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
components.add(node, 2);
|
|
223
|
+
},
|
|
224
|
+
FunctionExpression: function (_n) {
|
|
225
|
+
const node = utils.getParentComponent(_n);
|
|
226
|
+
if (!node) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
components.add(node, 1);
|
|
230
|
+
},
|
|
231
|
+
FunctionDeclaration: function (_n) {
|
|
232
|
+
const node = utils.getParentComponent(_n);
|
|
233
|
+
if (!node) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
components.add(node, 1);
|
|
237
|
+
},
|
|
238
|
+
ArrowFunctionExpression: function (_n) {
|
|
239
|
+
const node = utils.getParentComponent(_n);
|
|
240
|
+
if (!node) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (node.expression && utils.isReturningJSX(node)) {
|
|
244
|
+
components.add(node, 2);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
components.add(node, 1);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
ThisExpression: function (_n) {
|
|
251
|
+
const node = utils.getParentComponent(_n);
|
|
252
|
+
if (!node || !/Function/.test(node.type)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
components.add(node, 0);
|
|
256
|
+
},
|
|
257
|
+
ReturnStatement: function (node) {
|
|
258
|
+
if (!utils.isReturningJSX(node)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const parentNode = utils.getParentComponent(node);
|
|
262
|
+
if (!parentNode) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
components.add(parentNode, 2);
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
const ruleInstructions = rule(context, components, utils);
|
|
269
|
+
const updatedRuleInstructions = { ...ruleInstructions };
|
|
270
|
+
Object.keys(detectionInstructions).forEach((instruction) => {
|
|
271
|
+
updatedRuleInstructions[instruction] = (node) => {
|
|
272
|
+
detectionInstructions[instruction](node);
|
|
273
|
+
return ruleInstructions[instruction]
|
|
274
|
+
? ruleInstructions[instruction](node)
|
|
275
|
+
: undefined;
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
return updatedRuleInstructions;
|
|
279
|
+
}
|
|
280
|
+
export function detect(rule) {
|
|
281
|
+
return function (context) {
|
|
282
|
+
return componentRule(rule, context);
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
export class StyleSheets {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.styleSheets = {};
|
|
4
|
+
this.colorLiterals = [];
|
|
5
|
+
this.objectExpressions = [];
|
|
6
|
+
}
|
|
7
|
+
add(styleSheetName, properties) {
|
|
8
|
+
this.styleSheets[styleSheetName] = properties;
|
|
9
|
+
}
|
|
10
|
+
markAsUsed(fullyQualifiedName) {
|
|
11
|
+
const nameSplit = fullyQualifiedName.split('.');
|
|
12
|
+
const styleSheetName = nameSplit[0];
|
|
13
|
+
const styleSheetProperty = nameSplit[1];
|
|
14
|
+
if (this.styleSheets[styleSheetName]) {
|
|
15
|
+
this.styleSheets[styleSheetName] = this.styleSheets[styleSheetName].filter((property) => property.key.name !== styleSheetProperty);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
getUnusedReferences() {
|
|
19
|
+
return this.styleSheets;
|
|
20
|
+
}
|
|
21
|
+
addColorLiterals(expressions) {
|
|
22
|
+
this.colorLiterals = this.colorLiterals.concat(expressions);
|
|
23
|
+
}
|
|
24
|
+
getColorLiterals() {
|
|
25
|
+
return this.colorLiterals;
|
|
26
|
+
}
|
|
27
|
+
addObjectExpressions(expressions) {
|
|
28
|
+
this.objectExpressions = this.objectExpressions.concat(expressions);
|
|
29
|
+
}
|
|
30
|
+
getObjectExpressions() {
|
|
31
|
+
return this.objectExpressions;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
let currentContent;
|
|
35
|
+
const getSourceCode = (node) => currentContent.sourceCode.getText(node);
|
|
36
|
+
const getStyleSheetObjectNames = (settings) => settings['react-native/style-sheet-object-names'] || ['StyleSheet'];
|
|
37
|
+
export const astHelpers = {
|
|
38
|
+
containsStyleSheetObject: function (node, objectNames) {
|
|
39
|
+
return Boolean(node &&
|
|
40
|
+
node.type === 'CallExpression' &&
|
|
41
|
+
node.callee &&
|
|
42
|
+
node.callee.object &&
|
|
43
|
+
node.callee.object.name &&
|
|
44
|
+
objectNames.includes(node.callee.object.name));
|
|
45
|
+
},
|
|
46
|
+
containsCreateCall: function (node) {
|
|
47
|
+
return Boolean(node &&
|
|
48
|
+
node.callee &&
|
|
49
|
+
node.callee.property &&
|
|
50
|
+
node.callee.property.name === 'create');
|
|
51
|
+
},
|
|
52
|
+
isStyleSheetDeclaration: function (node, settings) {
|
|
53
|
+
const objectNames = getStyleSheetObjectNames(settings);
|
|
54
|
+
return Boolean(astHelpers.containsStyleSheetObject(node, objectNames) &&
|
|
55
|
+
astHelpers.containsCreateCall(node));
|
|
56
|
+
},
|
|
57
|
+
getStyleSheetName: function (node) {
|
|
58
|
+
if (node && node.parent && node.parent.id) {
|
|
59
|
+
return node.parent.id.name;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
getStyleDeclarations: function (node) {
|
|
63
|
+
if (node &&
|
|
64
|
+
node.type === 'CallExpression' &&
|
|
65
|
+
node.arguments &&
|
|
66
|
+
node.arguments[0] &&
|
|
67
|
+
node.arguments[0].properties) {
|
|
68
|
+
return node.arguments[0].properties.filter((property) => property.type === 'Property');
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
},
|
|
72
|
+
getStyleDeclarationsChunks: function (node) {
|
|
73
|
+
if (node &&
|
|
74
|
+
node.type === 'CallExpression' &&
|
|
75
|
+
node.arguments &&
|
|
76
|
+
node.arguments[0] &&
|
|
77
|
+
node.arguments[0].properties) {
|
|
78
|
+
const { properties } = node.arguments[0];
|
|
79
|
+
const result = [];
|
|
80
|
+
let chunk = [];
|
|
81
|
+
for (let i = 0; i < properties.length; i += 1) {
|
|
82
|
+
const property = properties[i];
|
|
83
|
+
if (property.type === 'Property') {
|
|
84
|
+
chunk.push(property);
|
|
85
|
+
}
|
|
86
|
+
else if (chunk.length) {
|
|
87
|
+
result.push(chunk);
|
|
88
|
+
chunk = [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (chunk.length) {
|
|
92
|
+
result.push(chunk);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
},
|
|
98
|
+
getPropertiesChunks: function (properties) {
|
|
99
|
+
const result = [];
|
|
100
|
+
let chunk = [];
|
|
101
|
+
for (let i = 0; i < properties.length; i += 1) {
|
|
102
|
+
const property = properties[i];
|
|
103
|
+
if (property.type === 'Property') {
|
|
104
|
+
chunk.push(property);
|
|
105
|
+
}
|
|
106
|
+
else if (chunk.length) {
|
|
107
|
+
result.push(chunk);
|
|
108
|
+
chunk = [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (chunk.length) {
|
|
112
|
+
result.push(chunk);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
},
|
|
116
|
+
getExpressionIdentifier: function (node) {
|
|
117
|
+
if (node) {
|
|
118
|
+
switch (node.type) {
|
|
119
|
+
case 'Identifier':
|
|
120
|
+
return node.name;
|
|
121
|
+
case 'Literal':
|
|
122
|
+
return node.value;
|
|
123
|
+
case 'TemplateLiteral':
|
|
124
|
+
return node.quasis.reduce((result, quasi, index) => result +
|
|
125
|
+
quasi.value.cooked +
|
|
126
|
+
astHelpers.getExpressionIdentifier(node.expressions[index]), '');
|
|
127
|
+
default:
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return '';
|
|
132
|
+
},
|
|
133
|
+
getStylePropertyIdentifier: function (node) {
|
|
134
|
+
if (node && node.key) {
|
|
135
|
+
return astHelpers.getExpressionIdentifier(node.key);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
isStyleAttribute: function (node) {
|
|
139
|
+
return Boolean(node.type === 'JSXAttribute' &&
|
|
140
|
+
node.name &&
|
|
141
|
+
node.name.name &&
|
|
142
|
+
node.name.name.toLowerCase().includes('style'));
|
|
143
|
+
},
|
|
144
|
+
collectStyleObjectExpressions: function (node, context) {
|
|
145
|
+
currentContent = context;
|
|
146
|
+
if (astHelpers.hasArrayOfStyleReferences(node)) {
|
|
147
|
+
const styleReferenceContainers = node.expression.elements;
|
|
148
|
+
return astHelpers.collectStyleObjectExpressionFromContainers(styleReferenceContainers);
|
|
149
|
+
}
|
|
150
|
+
if (node && node.expression) {
|
|
151
|
+
return astHelpers.getStyleObjectExpressionFromNode(node.expression);
|
|
152
|
+
}
|
|
153
|
+
return [];
|
|
154
|
+
},
|
|
155
|
+
collectColorLiterals: function (node, context) {
|
|
156
|
+
if (!node) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
currentContent = context;
|
|
160
|
+
if (astHelpers.hasArrayOfStyleReferences(node)) {
|
|
161
|
+
const styleReferenceContainers = node.expression.elements;
|
|
162
|
+
return astHelpers.collectColorLiteralsFromContainers(styleReferenceContainers);
|
|
163
|
+
}
|
|
164
|
+
if (node.type === 'ObjectExpression') {
|
|
165
|
+
return astHelpers.getColorLiteralsFromNode(node);
|
|
166
|
+
}
|
|
167
|
+
return astHelpers.getColorLiteralsFromNode(node.expression);
|
|
168
|
+
},
|
|
169
|
+
collectStyleObjectExpressionFromContainers: function (nodes) {
|
|
170
|
+
let objectExpressions = [];
|
|
171
|
+
nodes.forEach((node) => {
|
|
172
|
+
objectExpressions = objectExpressions.concat(astHelpers.getStyleObjectExpressionFromNode(node));
|
|
173
|
+
});
|
|
174
|
+
return objectExpressions;
|
|
175
|
+
},
|
|
176
|
+
collectColorLiteralsFromContainers: function (nodes) {
|
|
177
|
+
let colorLiterals = [];
|
|
178
|
+
nodes.forEach((node) => {
|
|
179
|
+
colorLiterals = colorLiterals.concat(astHelpers.getColorLiteralsFromNode(node));
|
|
180
|
+
});
|
|
181
|
+
return colorLiterals;
|
|
182
|
+
},
|
|
183
|
+
getStyleReferenceFromNode: function (node) {
|
|
184
|
+
let styleReference;
|
|
185
|
+
let leftStyleReferences;
|
|
186
|
+
let rightStyleReferences;
|
|
187
|
+
if (!node) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
switch (node.type) {
|
|
191
|
+
case 'MemberExpression':
|
|
192
|
+
styleReference = astHelpers.getStyleReferenceFromExpression(node);
|
|
193
|
+
return [styleReference];
|
|
194
|
+
case 'LogicalExpression':
|
|
195
|
+
leftStyleReferences = astHelpers.getStyleReferenceFromNode(node.left);
|
|
196
|
+
rightStyleReferences = astHelpers.getStyleReferenceFromNode(node.right);
|
|
197
|
+
return [].concat(leftStyleReferences).concat(rightStyleReferences);
|
|
198
|
+
case 'ConditionalExpression':
|
|
199
|
+
leftStyleReferences = astHelpers.getStyleReferenceFromNode(node.consequent);
|
|
200
|
+
rightStyleReferences = astHelpers.getStyleReferenceFromNode(node.alternate);
|
|
201
|
+
return [].concat(leftStyleReferences).concat(rightStyleReferences);
|
|
202
|
+
default:
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
getStyleObjectExpressionFromNode: function (node) {
|
|
207
|
+
let leftStyleObjectExpression;
|
|
208
|
+
let rightStyleObjectExpression;
|
|
209
|
+
if (!node) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
if (node.type === 'ObjectExpression') {
|
|
213
|
+
return [astHelpers.getStyleObjectFromExpression(node)];
|
|
214
|
+
}
|
|
215
|
+
switch (node.type) {
|
|
216
|
+
case 'LogicalExpression':
|
|
217
|
+
leftStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.left);
|
|
218
|
+
rightStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.right);
|
|
219
|
+
return []
|
|
220
|
+
.concat(leftStyleObjectExpression)
|
|
221
|
+
.concat(rightStyleObjectExpression);
|
|
222
|
+
case 'ConditionalExpression':
|
|
223
|
+
leftStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.consequent);
|
|
224
|
+
rightStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.alternate);
|
|
225
|
+
return []
|
|
226
|
+
.concat(leftStyleObjectExpression)
|
|
227
|
+
.concat(rightStyleObjectExpression);
|
|
228
|
+
default:
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
getColorLiteralsFromNode: function (node) {
|
|
233
|
+
let leftColorLiterals;
|
|
234
|
+
let rightColorLiterals;
|
|
235
|
+
if (!node) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
if (node.type === 'ObjectExpression') {
|
|
239
|
+
return [astHelpers.getColorLiteralsFromExpression(node)];
|
|
240
|
+
}
|
|
241
|
+
switch (node.type) {
|
|
242
|
+
case 'LogicalExpression':
|
|
243
|
+
leftColorLiterals = astHelpers.getColorLiteralsFromNode(node.left);
|
|
244
|
+
rightColorLiterals = astHelpers.getColorLiteralsFromNode(node.right);
|
|
245
|
+
return [].concat(leftColorLiterals).concat(rightColorLiterals);
|
|
246
|
+
case 'ConditionalExpression':
|
|
247
|
+
leftColorLiterals = astHelpers.getColorLiteralsFromNode(node.consequent);
|
|
248
|
+
rightColorLiterals = astHelpers.getColorLiteralsFromNode(node.alternate);
|
|
249
|
+
return [].concat(leftColorLiterals).concat(rightColorLiterals);
|
|
250
|
+
default:
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
hasArrayOfStyleReferences: function (node) {
|
|
255
|
+
return (node &&
|
|
256
|
+
Boolean(node.type === 'JSXExpressionContainer' &&
|
|
257
|
+
node.expression &&
|
|
258
|
+
node.expression.type === 'ArrayExpression'));
|
|
259
|
+
},
|
|
260
|
+
getStyleReferenceFromExpression: function (node) {
|
|
261
|
+
const result = [];
|
|
262
|
+
const name = astHelpers.getObjectName(node);
|
|
263
|
+
if (name) {
|
|
264
|
+
result.push(name);
|
|
265
|
+
}
|
|
266
|
+
const property = astHelpers.getPropertyName(node);
|
|
267
|
+
if (property) {
|
|
268
|
+
result.push(property);
|
|
269
|
+
}
|
|
270
|
+
return result.join('.');
|
|
271
|
+
},
|
|
272
|
+
getStyleObjectFromExpression: function (node) {
|
|
273
|
+
const obj = {};
|
|
274
|
+
let invalid = false;
|
|
275
|
+
if (node.properties && node.properties.length) {
|
|
276
|
+
node.properties.forEach((p) => {
|
|
277
|
+
if (!p.value || !p.key) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (p.value.type === 'Literal') {
|
|
281
|
+
invalid = true;
|
|
282
|
+
obj[p.key.name] = p.value.value;
|
|
283
|
+
}
|
|
284
|
+
else if (p.value.type === 'ConditionalExpression') {
|
|
285
|
+
const innerNode = p.value;
|
|
286
|
+
if (innerNode.consequent.type === 'Literal' ||
|
|
287
|
+
innerNode.alternate.type === 'Literal') {
|
|
288
|
+
invalid = true;
|
|
289
|
+
obj[p.key.name] = getSourceCode(innerNode);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (p.value.type === 'UnaryExpression' &&
|
|
293
|
+
p.value.operator === '-' &&
|
|
294
|
+
p.value.argument.type === 'Literal') {
|
|
295
|
+
invalid = true;
|
|
296
|
+
obj[p.key.name] = -1 * p.value.argument.value;
|
|
297
|
+
}
|
|
298
|
+
else if (p.value.type === 'UnaryExpression' &&
|
|
299
|
+
p.value.operator === '+' &&
|
|
300
|
+
p.value.argument.type === 'Literal') {
|
|
301
|
+
invalid = true;
|
|
302
|
+
obj[p.key.name] = p.value.argument.value;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return invalid ? { expression: obj, node: node } : undefined;
|
|
307
|
+
},
|
|
308
|
+
getColorLiteralsFromExpression: function (node) {
|
|
309
|
+
const obj = {};
|
|
310
|
+
let invalid = false;
|
|
311
|
+
if (node.properties && node.properties.length) {
|
|
312
|
+
node.properties.forEach((p) => {
|
|
313
|
+
if (p.key &&
|
|
314
|
+
p.key.name &&
|
|
315
|
+
p.key.name.toLowerCase().indexOf('color') !== -1) {
|
|
316
|
+
if (p.value.type === 'Literal') {
|
|
317
|
+
invalid = true;
|
|
318
|
+
obj[p.key.name] = p.value.value;
|
|
319
|
+
}
|
|
320
|
+
else if (p.value.type === 'ConditionalExpression') {
|
|
321
|
+
const innerNode = p.value;
|
|
322
|
+
if (innerNode.consequent.type === 'Literal' ||
|
|
323
|
+
innerNode.alternate.type === 'Literal') {
|
|
324
|
+
invalid = true;
|
|
325
|
+
obj[p.key.name] = getSourceCode(innerNode);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return invalid ? { expression: obj, node: node } : undefined;
|
|
332
|
+
},
|
|
333
|
+
getObjectName: function (node) {
|
|
334
|
+
if (node && node.object && node.object.name) {
|
|
335
|
+
return node.object.name;
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
getPropertyName: function (node) {
|
|
339
|
+
if (node && node.property && node.property.name) {
|
|
340
|
+
return node.property.name;
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
getPotentialStyleReferenceFromMemberExpression: function (node) {
|
|
344
|
+
if (node &&
|
|
345
|
+
node.object &&
|
|
346
|
+
node.object.type === 'Identifier' &&
|
|
347
|
+
node.object.name &&
|
|
348
|
+
node.property &&
|
|
349
|
+
node.property.type === 'Identifier' &&
|
|
350
|
+
node.property.name &&
|
|
351
|
+
node.parent.type !== 'MemberExpression') {
|
|
352
|
+
return [node.object.name, node.property.name].join('.');
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
isEitherShortHand: function (property1, property2) {
|
|
356
|
+
const shorthands = ['margin', 'padding', 'border', 'flex'];
|
|
357
|
+
if (shorthands.includes(property1)) {
|
|
358
|
+
return property2.startsWith(property1);
|
|
359
|
+
}
|
|
360
|
+
if (shorthands.includes(property2)) {
|
|
361
|
+
return property1.startsWith(property2);
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
},
|
|
365
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oxlint-plugin-react-native",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React Native specific linting rules for Oxlint",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"oxlint",
|
|
7
|
+
"oxlint-plugin",
|
|
8
|
+
"oxlintplugin",
|
|
9
|
+
"react",
|
|
10
|
+
"react native",
|
|
11
|
+
"react-native"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/huextrat/oxlint-plugin-react-native",
|
|
14
|
+
"bugs": "https://github.com/huextrat/oxlint-plugin-react-native/issues",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Hugo Extrat <extrat.h@gmail.com>",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/huextrat/oxlint-plugin-react-native"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"README.md",
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"lint": "oxlint --type-aware --type-check --report-unused-disable-directives",
|
|
31
|
+
"lint:fix": "oxlint --type-aware --type-check --report-unused-disable-directives --fix",
|
|
32
|
+
"format": "oxfmt",
|
|
33
|
+
"format:check": "oxfmt --check",
|
|
34
|
+
"test": "jest",
|
|
35
|
+
"test:watch": "jest --watch"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@oxlint/plugins": "1.43.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@babel/core": "7.29.0",
|
|
42
|
+
"@babel/preset-env": "7.29.0",
|
|
43
|
+
"@babel/preset-typescript": "7.28.5",
|
|
44
|
+
"@types/jest": "30.0.0",
|
|
45
|
+
"@types/node": "25.2.0",
|
|
46
|
+
"jest": "30.2.0",
|
|
47
|
+
"oxfmt": "^0.28.0",
|
|
48
|
+
"oxlint": "1.43.0",
|
|
49
|
+
"oxlint-tsgolint": "^0.11.4",
|
|
50
|
+
"typescript": "5.9.3"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"oxlint": ">=1.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|