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 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
+ }