oxlint-plugin-react-native 0.0.1 → 0.2.0

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Lint rules for [React Native](https://reactnative.dev/) projects, built for [Oxlint](https://github.com/oxc-project/oxc).
4
4
 
5
+ Rules are based on [eslint-plugin-react-native](https://github.com/Intellicode/eslint-plugin-react-native) by Intellicode, ported to Oxlint. This plugin may evolve with new rules, improvements, or updates over time.
6
+
5
7
  ---
6
8
 
7
9
  ## Installation
@@ -50,7 +52,7 @@ In your Oxlint config (e.g. `.oxlintrc.json`), register the plugin by **name** a
50
52
  | [no-inline-styles](docs/no-inline-styles.md) | Disallow inline style objects; prefer `StyleSheet.create` | — |
51
53
  | [no-raw-text](docs/no-raw-text.md) | Require text inside `<Text>` (or allowed components) | — |
52
54
  | [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` | |
55
+ | [no-unused-styles](docs/no-unused-styles.md) | Report unused styles from `StyleSheet.create` | |
54
56
  | [sort-styles](docs/sort-styles.md) | Enforce order of class names and style properties | ✅ |
55
57
 
56
58
  Each rule is documented in the [docs](docs/) folder with examples and options.
package/dist/index.js CHANGED
@@ -1,19 +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';
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
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,
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
15
  };
16
16
  export default eslintCompatPlugin({
17
- meta: { name: 'oxlint-plugin-react-native' },
17
+ meta: { name: "oxlint-plugin-react-native" },
18
18
  rules: allRules,
19
19
  });
@@ -1,6 +1,6 @@
1
- import { detect } from '../util/Components.js';
2
- import { StyleSheets, astHelpers } from '../util/stylesheet.js';
3
- import * as util from 'util';
1
+ import { detect } from "../util/Components.js";
2
+ import { StyleSheets, astHelpers } from "../util/stylesheet.js";
3
+ import * as util from "util";
4
4
  const rule = detect((context) => {
5
5
  let styleSheets;
6
6
  return {
@@ -24,7 +24,7 @@ const rule = detect((context) => {
24
24
  styleSheets.addColorLiterals(literals);
25
25
  }
26
26
  },
27
- 'Program:exit': () => {
27
+ "Program:exit": () => {
28
28
  const colorLiterals = styleSheets.getColorLiterals();
29
29
  if (colorLiterals) {
30
30
  colorLiterals.forEach((style) => {
@@ -32,7 +32,7 @@ const rule = detect((context) => {
32
32
  const expression = util.inspect(style.expression);
33
33
  context.report({
34
34
  node: style.node,
35
- message: 'Color literal: {{expression}}',
35
+ message: "Color literal: {{expression}}",
36
36
  data: { expression },
37
37
  });
38
38
  }
@@ -45,5 +45,5 @@ export default {
45
45
  meta: {
46
46
  schema: [],
47
47
  },
48
- createOnce: rule
48
+ createOnce: rule,
49
49
  };
@@ -1,6 +1,6 @@
1
- import { detect } from '../util/Components.js';
2
- import { StyleSheets, astHelpers } from '../util/stylesheet.js';
3
- import * as util from 'util';
1
+ import { detect } from "../util/Components.js";
2
+ import { StyleSheets, astHelpers } from "../util/stylesheet.js";
3
+ import * as util from "util";
4
4
  const rule = detect((context) => {
5
5
  // Setup state per-file (createOnce)
6
6
  let styleSheets;
@@ -14,7 +14,7 @@ const rule = detect((context) => {
14
14
  styleSheets.addObjectExpressions(styles);
15
15
  }
16
16
  },
17
- 'Program:exit': () => {
17
+ "Program:exit": () => {
18
18
  const inlineStyles = styleSheets.getObjectExpressions();
19
19
  if (inlineStyles) {
20
20
  inlineStyles.forEach((style) => {
@@ -22,7 +22,7 @@ const rule = detect((context) => {
22
22
  const expression = util.inspect(style.expression);
23
23
  context.report({
24
24
  node: style.node,
25
- message: 'Inline style: {{expression}}',
25
+ message: "Inline style: {{expression}}",
26
26
  data: { expression },
27
27
  });
28
28
  }
@@ -35,5 +35,5 @@ export default {
35
35
  meta: {
36
36
  schema: [],
37
37
  },
38
- createOnce: rule
38
+ createOnce: rule,
39
39
  };
@@ -1,24 +1,24 @@
1
1
  const elementName = (node) => {
2
2
  const reversedIdentifiers = [];
3
- if (node.type === 'JSXElement' &&
4
- node.openingElement.type === 'JSXOpeningElement') {
3
+ if (node.type === "JSXElement" &&
4
+ node.openingElement.type === "JSXOpeningElement") {
5
5
  let object = node.openingElement.name;
6
- while (object.type === 'JSXMemberExpression') {
7
- if (object.property.type === 'JSXIdentifier') {
6
+ while (object.type === "JSXMemberExpression") {
7
+ if (object.property.type === "JSXIdentifier") {
8
8
  reversedIdentifiers.push(object.property.name);
9
9
  }
10
10
  object = object.object;
11
11
  }
12
- if (object.type === 'JSXIdentifier') {
12
+ if (object.type === "JSXIdentifier") {
13
13
  reversedIdentifiers.push(object.name);
14
14
  }
15
15
  }
16
- return reversedIdentifiers.reverse().join('.');
16
+ return reversedIdentifiers.reverse().join(".");
17
17
  };
18
18
  const hasAllowedParent = (parent, allowedElements) => {
19
19
  let curNode = parent;
20
20
  while (curNode) {
21
- if (curNode.type === 'JSXElement') {
21
+ if (curNode.type === "JSXElement") {
22
22
  const name = elementName(curNode);
23
23
  if (allowedElements.includes(name)) {
24
24
  return true;
@@ -36,50 +36,50 @@ const rule = (context) => {
36
36
  const options = context.options[0] || {};
37
37
  const skippedElements = options.skip ? options.skip : [];
38
38
  _allowedElements = [
39
- 'Text',
40
- 'TSpan',
41
- 'StyledText',
42
- 'Animated.Text',
39
+ "Text",
40
+ "TSpan",
41
+ "StyledText",
42
+ "Animated.Text",
43
43
  ].concat(skippedElements);
44
44
  }
45
45
  return _allowedElements;
46
46
  }
47
47
  const report = (node) => {
48
- const errorValue = node.type === 'TemplateLiteral'
48
+ const errorValue = node.type === "TemplateLiteral"
49
49
  ? `TemplateLiteral: ${node.expressions[0].name}`
50
50
  : node.value.trim();
51
- const formattedErrorValue = errorValue.length > 0 ? `Raw text (${errorValue})` : 'Whitespace(s)';
51
+ const formattedErrorValue = errorValue.length > 0 ? `Raw text (${errorValue})` : "Whitespace(s)";
52
52
  context.report({
53
53
  node,
54
54
  message: `${formattedErrorValue} cannot be used outside of a <Text> tag`,
55
55
  });
56
56
  };
57
- const hasOnlyLineBreak = (value) => /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ''));
57
+ const hasOnlyLineBreak = (value) => /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ""));
58
58
  const getValidation = (node) => !hasAllowedParent(node.parent, getAllowedElements());
59
59
  return {
60
60
  Literal(node) {
61
61
  const parentType = node.parent.type;
62
- const onlyFor = ['JSXExpressionContainer', 'JSXElement'];
63
- if (typeof node.value !== 'string' ||
62
+ const onlyFor = ["JSXExpressionContainer", "JSXElement"];
63
+ if (typeof node.value !== "string" ||
64
64
  hasOnlyLineBreak(node.value) ||
65
65
  !onlyFor.includes(parentType) ||
66
- (node.parent.parent && node.parent.parent.type === 'JSXAttribute'))
66
+ (node.parent.parent && node.parent.parent.type === "JSXAttribute"))
67
67
  return;
68
- const isStringLiteral = parentType === 'JSXExpressionContainer';
68
+ const isStringLiteral = parentType === "JSXExpressionContainer";
69
69
  if (getValidation(isStringLiteral ? node.parent : node)) {
70
70
  report(node);
71
71
  }
72
72
  },
73
73
  JSXText(node) {
74
- if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value))
74
+ if (typeof node.value !== "string" || hasOnlyLineBreak(node.value))
75
75
  return;
76
76
  if (getValidation(node)) {
77
77
  report(node);
78
78
  }
79
79
  },
80
80
  TemplateLiteral(node) {
81
- if (node.parent.type !== 'JSXExpressionContainer' ||
82
- (node.parent.parent && node.parent.parent.type === 'JSXAttribute'))
81
+ if (node.parent.type !== "JSXExpressionContainer" ||
82
+ (node.parent.parent && node.parent.parent.type === "JSXAttribute"))
83
83
  return;
84
84
  if (getValidation(node.parent)) {
85
85
  report(node);
@@ -91,12 +91,12 @@ export default {
91
91
  meta: {
92
92
  schema: [
93
93
  {
94
- type: 'object',
94
+ type: "object",
95
95
  properties: {
96
96
  skip: {
97
- type: 'array',
97
+ type: "array",
98
98
  items: {
99
- type: 'string',
99
+ type: "string",
100
100
  },
101
101
  },
102
102
  },
@@ -2,7 +2,7 @@ const rule = (context) => {
2
2
  function reportNode(JSXExpressionNode) {
3
3
  context.report({
4
4
  node: JSXExpressionNode,
5
- message: 'Single element style arrays are not necessary and cause unnecessary re-renders',
5
+ message: "Single element style arrays are not necessary and cause unnecessary re-renders",
6
6
  fix(fixer) {
7
7
  const realStyleNode = JSXExpressionNode.value.expression.elements[0];
8
8
  const styleSource = context.sourceCode.getText(realStyleNode);
@@ -15,11 +15,11 @@ const rule = (context) => {
15
15
  // --------------------------------------------------------------------------
16
16
  return {
17
17
  JSXAttribute(node) {
18
- if (node.name.name !== 'style')
18
+ if (node.name.name !== "style")
19
19
  return;
20
20
  if (!node.value.expression)
21
21
  return;
22
- if (node.value.expression.type !== 'ArrayExpression')
22
+ if (node.value.expression.type !== "ArrayExpression")
23
23
  return;
24
24
  if (node.value.expression.elements.length === 1) {
25
25
  reportNode(node);
@@ -30,12 +30,12 @@ const rule = (context) => {
30
30
  export default {
31
31
  meta: {
32
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',
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
35
  recommended: false,
36
- url: '',
36
+ url: "",
37
37
  },
38
- fixable: 'code',
38
+ fixable: "code",
39
39
  },
40
40
  createOnce: rule,
41
41
  };
@@ -1,5 +1,5 @@
1
- import { detect } from '../util/Components.js';
2
- import { StyleSheets, astHelpers } from '../util/stylesheet.js';
1
+ import { detect } from "../util/Components.js";
2
+ import { StyleSheets, astHelpers } from "../util/stylesheet.js";
3
3
  const rule = detect((context, components) => {
4
4
  let styleSheets;
5
5
  let styleReferences;
@@ -9,12 +9,40 @@ const rule = detect((context, components) => {
9
9
  const styles = unusedStyles[key];
10
10
  styles.forEach((node) => {
11
11
  const message = [
12
- 'Unused style detected: ',
12
+ "Unused style detected: ",
13
13
  key,
14
- '.',
14
+ ".",
15
15
  node.key.name,
16
- ].join('');
17
- context.report({ node, message });
16
+ ].join("");
17
+ context.report({
18
+ node,
19
+ message,
20
+ fix(fixer) {
21
+ const parent = node.parent;
22
+ if (!parent || !parent.properties) {
23
+ return fixer.remove(node);
24
+ }
25
+ const properties = parent.properties;
26
+ const index = properties.indexOf(node);
27
+ if (index === -1)
28
+ return fixer.remove(node);
29
+ let removeStart;
30
+ let removeEnd;
31
+ if (index === 0) {
32
+ removeStart = node.range[0];
33
+ removeEnd =
34
+ properties.length > 1
35
+ ? properties[1].range[0]
36
+ : node.range[1];
37
+ }
38
+ else {
39
+ const prev = properties[index - 1];
40
+ removeStart = prev.range[1];
41
+ removeEnd = node.range[1];
42
+ }
43
+ return fixer.removeRange([removeStart, removeEnd]);
44
+ },
45
+ });
18
46
  });
19
47
  }
20
48
  });
@@ -36,10 +64,22 @@ const rule = detect((context, components) => {
36
64
  const styles = astHelpers.getStyleDeclarations(node);
37
65
  if (styleSheetName) {
38
66
  styleSheets.add(styleSheetName, styles);
67
+ if (astHelpers.isStyleSheetExported(node)) {
68
+ styleSheets.markAsExported(styleSheetName);
69
+ }
39
70
  }
40
71
  }
41
72
  },
42
- 'Program:exit': function () {
73
+ ExportNamedDeclaration: function (node) {
74
+ if (node.specifiers) {
75
+ for (const spec of node.specifiers) {
76
+ const name = spec.local && spec.local.name;
77
+ if (name)
78
+ styleSheets.markAsExported(name);
79
+ }
80
+ }
81
+ },
82
+ "Program:exit": function () {
43
83
  const list = components.all();
44
84
  if (Object.keys(list).length > 0) {
45
85
  styleReferences.forEach((reference) => {
@@ -53,6 +93,7 @@ const rule = detect((context, components) => {
53
93
  export default {
54
94
  meta: {
55
95
  schema: [],
96
+ fixable: "code",
56
97
  },
57
98
  createOnce: rule,
58
99
  };
@@ -1,10 +1,10 @@
1
- import { astHelpers } from '../util/stylesheet.js';
1
+ import { astHelpers } from "../util/stylesheet.js";
2
2
  const rule = (context) => {
3
3
  // Defer context.options and context.sourceCode to visitor (oxlint forbids in createOnce).
4
4
  function sort(array, order) {
5
5
  return [...array].sort((a, b) => {
6
- const identifierA = astHelpers.getStylePropertyIdentifier(a) || '';
7
- const identifierB = astHelpers.getStylePropertyIdentifier(b) || '';
6
+ const identifierA = astHelpers.getStylePropertyIdentifier(a) || "";
7
+ const identifierB = astHelpers.getStylePropertyIdentifier(b) || "";
8
8
  let sortOrder = 0;
9
9
  if (astHelpers.isEitherShortHand(identifierA, identifierB)) {
10
10
  return a.range[0] - b.range[0];
@@ -15,7 +15,7 @@ const rule = (context) => {
15
15
  else if (identifierA > identifierB) {
16
16
  sortOrder = 1;
17
17
  }
18
- return sortOrder * (order === 'asc' ? 1 : -1);
18
+ return sortOrder * (order === "asc" ? 1 : -1);
19
19
  });
20
20
  }
21
21
  function report(array, type, node, prev, current, order, sourceCode) {
@@ -50,12 +50,12 @@ const rule = (context) => {
50
50
  for (let i = 1; i < array.length; i += 1) {
51
51
  const previous = array[i - 1];
52
52
  const current = array[i];
53
- if (previous.type !== 'Property' || current.type !== 'Property') {
53
+ if (previous.type !== "Property" || current.type !== "Property") {
54
54
  return;
55
55
  }
56
- const prevName = astHelpers.getStylePropertyIdentifier(previous) || '';
57
- const currentName = astHelpers.getStylePropertyIdentifier(current) || '';
58
- const oneIsShorthandForTheOther = arrayName === 'style properties' &&
56
+ const prevName = astHelpers.getStylePropertyIdentifier(previous) || "";
57
+ const currentName = astHelpers.getStylePropertyIdentifier(current) || "";
58
+ const oneIsShorthandForTheOther = arrayName === "style properties" &&
59
59
  astHelpers.isEitherShortHand(prevName, currentName);
60
60
  if (!oneIsShorthandForTheOther && !isValidOrder(prevName, currentName)) {
61
61
  return report(array, arrayName, node, previous, current, order, sourceCode);
@@ -64,11 +64,11 @@ const rule = (context) => {
64
64
  }
65
65
  return {
66
66
  CallExpression: function (node) {
67
- const order = context.options[0] || 'asc';
67
+ const order = context.options[0] || "asc";
68
68
  const options = context.options[1] || {};
69
69
  const { ignoreClassNames, ignoreStyleProperties } = options;
70
70
  const sourceCode = context.sourceCode;
71
- const isValidOrder = order === 'asc'
71
+ const isValidOrder = order === "asc"
72
72
  ? (a, b) => a <= b
73
73
  : (a, b) => a >= b;
74
74
  if (!astHelpers.isStyleSheetDeclaration(node, context.settings)) {
@@ -77,7 +77,7 @@ const rule = (context) => {
77
77
  const classDefinitionsChunks = astHelpers.getStyleDeclarationsChunks(node);
78
78
  if (!ignoreClassNames) {
79
79
  classDefinitionsChunks.forEach((classDefinitions) => {
80
- checkIsSorted(classDefinitions, 'class names', node, order, options, sourceCode, isValidOrder);
80
+ checkIsSorted(classDefinitions, "class names", node, order, options, sourceCode, isValidOrder);
81
81
  });
82
82
  }
83
83
  if (ignoreStyleProperties)
@@ -90,7 +90,7 @@ const rule = (context) => {
90
90
  }
91
91
  const stylePropertyChunks = astHelpers.getPropertiesChunks(styleProperties);
92
92
  stylePropertyChunks.forEach((stylePropertyChunk) => {
93
- checkIsSorted(stylePropertyChunk, 'style properties', node, order, options, sourceCode, isValidOrder);
93
+ checkIsSorted(stylePropertyChunk, "style properties", node, order, options, sourceCode, isValidOrder);
94
94
  });
95
95
  });
96
96
  });
@@ -99,19 +99,19 @@ const rule = (context) => {
99
99
  };
100
100
  export default {
101
101
  meta: {
102
- fixable: 'code',
102
+ fixable: "code",
103
103
  schema: [
104
104
  {
105
- enum: ['asc', 'desc'],
105
+ enum: ["asc", "desc"],
106
106
  },
107
107
  {
108
- type: 'object',
108
+ type: "object",
109
109
  properties: {
110
110
  ignoreClassNames: {
111
- type: 'boolean',
111
+ type: "boolean",
112
112
  },
113
113
  ignoreStyleProperties: {
114
- type: 'boolean',
114
+ type: "boolean",
115
115
  },
116
116
  },
117
117
  additionalProperties: false,
@@ -3,7 +3,7 @@ export class Components {
3
3
  this.list = {};
4
4
  }
5
5
  getId(node) {
6
- return node ? node.range.join(':') : '';
6
+ return node ? node.range.join(":") : "";
7
7
  }
8
8
  add(node, confidence) {
9
9
  const id = this.getId(node);
@@ -76,22 +76,22 @@ export function componentRule(rule, context) {
76
76
  isReturningJSX: function (node) {
77
77
  let property;
78
78
  switch (node.type) {
79
- case 'ReturnStatement':
80
- property = 'argument';
79
+ case "ReturnStatement":
80
+ property = "argument";
81
81
  break;
82
- case 'ArrowFunctionExpression':
83
- property = 'body';
82
+ case "ArrowFunctionExpression":
83
+ property = "body";
84
84
  break;
85
85
  default:
86
86
  return false;
87
87
  }
88
88
  const returnsJSX = node[property] &&
89
- (node[property].type === 'JSXElement' ||
90
- node[property].type === 'JSXFragment');
89
+ (node[property].type === "JSXElement" ||
90
+ node[property].type === "JSXFragment");
91
91
  const returnsReactCreateElement = node[property] &&
92
92
  node[property].callee &&
93
93
  node[property].callee.property &&
94
- node[property].callee.property.name === 'createElement';
94
+ node[property].callee.property.name === "createElement";
95
95
  return Boolean(returnsJSX || returnsReactCreateElement);
96
96
  },
97
97
  getParentComponent: function (_n) {
@@ -112,7 +112,7 @@ export function componentRule(rule, context) {
112
112
  },
113
113
  getParentES6Component: function (_n) {
114
114
  let scope = (context.sourceCode || context).getScope(_n);
115
- while (scope && scope.type !== 'class') {
115
+ while (scope && scope.type !== "class") {
116
116
  scope = scope.upper;
117
117
  }
118
118
  const node = scope && scope.block;
@@ -126,8 +126,8 @@ export function componentRule(rule, context) {
126
126
  while (scope) {
127
127
  const node = scope.block;
128
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';
129
+ const isNotMethod = !node.parent || node.parent.type !== "MethodDefinition";
130
+ const isNotArgument = !node.parent || node.parent.type !== "CallExpression";
131
131
  if (isFunction && isNotMethod && isNotArgument) {
132
132
  return node;
133
133
  }
@@ -144,10 +144,10 @@ export function componentRule(rule, context) {
144
144
  const componentPath = [];
145
145
  while (currentNode) {
146
146
  if (currentNode.property &&
147
- currentNode.property.type === 'Identifier') {
147
+ currentNode.property.type === "Identifier") {
148
148
  componentPath.push(currentNode.property.name);
149
149
  }
150
- if (currentNode.object && currentNode.object.type === 'Identifier') {
150
+ if (currentNode.object && currentNode.object.type === "Identifier") {
151
151
  componentPath.push(currentNode.object.name);
152
152
  }
153
153
  currentNode = currentNode.object;
@@ -171,9 +171,9 @@ export function componentRule(rule, context) {
171
171
  let defInScope;
172
172
  const { defs } = variableInScope;
173
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') {
174
+ if (defs[i].type === "ClassName" ||
175
+ defs[i].type === "FunctionName" ||
176
+ defs[i].type === "Variable") {
177
177
  defInScope = defs[i];
178
178
  break;
179
179
  }
@@ -1,5 +1,6 @@
1
1
  export class StyleSheets {
2
2
  constructor() {
3
+ this.exportedNames = new Set();
3
4
  this.styleSheets = {};
4
5
  this.colorLiterals = [];
5
6
  this.objectExpressions = [];
@@ -7,8 +8,11 @@ export class StyleSheets {
7
8
  add(styleSheetName, properties) {
8
9
  this.styleSheets[styleSheetName] = properties;
9
10
  }
11
+ markAsExported(styleSheetName) {
12
+ this.exportedNames.add(styleSheetName);
13
+ }
10
14
  markAsUsed(fullyQualifiedName) {
11
- const nameSplit = fullyQualifiedName.split('.');
15
+ const nameSplit = fullyQualifiedName.split(".");
12
16
  const styleSheetName = nameSplit[0];
13
17
  const styleSheetProperty = nameSplit[1];
14
18
  if (this.styleSheets[styleSheetName]) {
@@ -16,7 +20,13 @@ export class StyleSheets {
16
20
  }
17
21
  }
18
22
  getUnusedReferences() {
19
- return this.styleSheets;
23
+ const result = {};
24
+ for (const [name, properties] of Object.entries(this.styleSheets)) {
25
+ if (properties.length > 0 && !this.exportedNames.has(name)) {
26
+ result[name] = properties;
27
+ }
28
+ }
29
+ return result;
20
30
  }
21
31
  addColorLiterals(expressions) {
22
32
  this.colorLiterals = this.colorLiterals.concat(expressions);
@@ -33,11 +43,11 @@ export class StyleSheets {
33
43
  }
34
44
  let currentContent;
35
45
  const getSourceCode = (node) => currentContent.sourceCode.getText(node);
36
- const getStyleSheetObjectNames = (settings) => settings['react-native/style-sheet-object-names'] || ['StyleSheet'];
46
+ const getStyleSheetObjectNames = (settings) => settings["react-native/style-sheet-object-names"] || ["StyleSheet"];
37
47
  export const astHelpers = {
38
48
  containsStyleSheetObject: function (node, objectNames) {
39
49
  return Boolean(node &&
40
- node.type === 'CallExpression' &&
50
+ node.type === "CallExpression" &&
41
51
  node.callee &&
42
52
  node.callee.object &&
43
53
  node.callee.object.name &&
@@ -47,7 +57,7 @@ export const astHelpers = {
47
57
  return Boolean(node &&
48
58
  node.callee &&
49
59
  node.callee.property &&
50
- node.callee.property.name === 'create');
60
+ node.callee.property.name === "create");
51
61
  },
52
62
  isStyleSheetDeclaration: function (node, settings) {
53
63
  const objectNames = getStyleSheetObjectNames(settings);
@@ -59,19 +69,41 @@ export const astHelpers = {
59
69
  return node.parent.id.name;
60
70
  }
61
71
  },
72
+ /**
73
+ * Returns true if the StyleSheet.create call is part of an export, so its
74
+ * styles may be used in other files and should not be reported as unused.
75
+ */
76
+ isStyleSheetExported: function (node) {
77
+ if (!node)
78
+ return false;
79
+ // export default StyleSheet.create(...)
80
+ if (node.parent && node.parent.type === "ExportDefaultDeclaration")
81
+ return true;
82
+ const declarator = node.parent;
83
+ if (!declarator || declarator.type !== "VariableDeclarator")
84
+ return false;
85
+ const declaration = declarator.parent;
86
+ if (!declaration)
87
+ return false;
88
+ // export const styles = StyleSheet.create(...)
89
+ if (declaration.parent &&
90
+ declaration.parent.type === "ExportNamedDeclaration")
91
+ return true;
92
+ return false;
93
+ },
62
94
  getStyleDeclarations: function (node) {
63
95
  if (node &&
64
- node.type === 'CallExpression' &&
96
+ node.type === "CallExpression" &&
65
97
  node.arguments &&
66
98
  node.arguments[0] &&
67
99
  node.arguments[0].properties) {
68
- return node.arguments[0].properties.filter((property) => property.type === 'Property');
100
+ return node.arguments[0].properties.filter((property) => property.type === "Property");
69
101
  }
70
102
  return [];
71
103
  },
72
104
  getStyleDeclarationsChunks: function (node) {
73
105
  if (node &&
74
- node.type === 'CallExpression' &&
106
+ node.type === "CallExpression" &&
75
107
  node.arguments &&
76
108
  node.arguments[0] &&
77
109
  node.arguments[0].properties) {
@@ -80,7 +112,7 @@ export const astHelpers = {
80
112
  let chunk = [];
81
113
  for (let i = 0; i < properties.length; i += 1) {
82
114
  const property = properties[i];
83
- if (property.type === 'Property') {
115
+ if (property.type === "Property") {
84
116
  chunk.push(property);
85
117
  }
86
118
  else if (chunk.length) {
@@ -100,7 +132,7 @@ export const astHelpers = {
100
132
  let chunk = [];
101
133
  for (let i = 0; i < properties.length; i += 1) {
102
134
  const property = properties[i];
103
- if (property.type === 'Property') {
135
+ if (property.type === "Property") {
104
136
  chunk.push(property);
105
137
  }
106
138
  else if (chunk.length) {
@@ -116,19 +148,19 @@ export const astHelpers = {
116
148
  getExpressionIdentifier: function (node) {
117
149
  if (node) {
118
150
  switch (node.type) {
119
- case 'Identifier':
151
+ case "Identifier":
120
152
  return node.name;
121
- case 'Literal':
153
+ case "Literal":
122
154
  return node.value;
123
- case 'TemplateLiteral':
155
+ case "TemplateLiteral":
124
156
  return node.quasis.reduce((result, quasi, index) => result +
125
157
  quasi.value.cooked +
126
- astHelpers.getExpressionIdentifier(node.expressions[index]), '');
158
+ astHelpers.getExpressionIdentifier(node.expressions[index]), "");
127
159
  default:
128
- return '';
160
+ return "";
129
161
  }
130
162
  }
131
- return '';
163
+ return "";
132
164
  },
133
165
  getStylePropertyIdentifier: function (node) {
134
166
  if (node && node.key) {
@@ -136,10 +168,10 @@ export const astHelpers = {
136
168
  }
137
169
  },
138
170
  isStyleAttribute: function (node) {
139
- return Boolean(node.type === 'JSXAttribute' &&
171
+ return Boolean(node.type === "JSXAttribute" &&
140
172
  node.name &&
141
173
  node.name.name &&
142
- node.name.name.toLowerCase().includes('style'));
174
+ node.name.name.toLowerCase().includes("style"));
143
175
  },
144
176
  collectStyleObjectExpressions: function (node, context) {
145
177
  currentContent = context;
@@ -161,7 +193,7 @@ export const astHelpers = {
161
193
  const styleReferenceContainers = node.expression.elements;
162
194
  return astHelpers.collectColorLiteralsFromContainers(styleReferenceContainers);
163
195
  }
164
- if (node.type === 'ObjectExpression') {
196
+ if (node.type === "ObjectExpression") {
165
197
  return astHelpers.getColorLiteralsFromNode(node);
166
198
  }
167
199
  return astHelpers.getColorLiteralsFromNode(node.expression);
@@ -188,17 +220,21 @@ export const astHelpers = {
188
220
  return [];
189
221
  }
190
222
  switch (node.type) {
191
- case 'MemberExpression':
223
+ case "MemberExpression":
192
224
  styleReference = astHelpers.getStyleReferenceFromExpression(node);
193
225
  return [styleReference];
194
- case 'LogicalExpression':
226
+ case "LogicalExpression":
195
227
  leftStyleReferences = astHelpers.getStyleReferenceFromNode(node.left);
196
228
  rightStyleReferences = astHelpers.getStyleReferenceFromNode(node.right);
197
- return [].concat(leftStyleReferences).concat(rightStyleReferences);
198
- case 'ConditionalExpression':
229
+ return []
230
+ .concat(leftStyleReferences)
231
+ .concat(rightStyleReferences);
232
+ case "ConditionalExpression":
199
233
  leftStyleReferences = astHelpers.getStyleReferenceFromNode(node.consequent);
200
234
  rightStyleReferences = astHelpers.getStyleReferenceFromNode(node.alternate);
201
- return [].concat(leftStyleReferences).concat(rightStyleReferences);
235
+ return []
236
+ .concat(leftStyleReferences)
237
+ .concat(rightStyleReferences);
202
238
  default:
203
239
  return [];
204
240
  }
@@ -209,19 +245,21 @@ export const astHelpers = {
209
245
  if (!node) {
210
246
  return [];
211
247
  }
212
- if (node.type === 'ObjectExpression') {
248
+ if (node.type === "ObjectExpression") {
213
249
  return [astHelpers.getStyleObjectFromExpression(node)];
214
250
  }
215
251
  switch (node.type) {
216
- case 'LogicalExpression':
252
+ case "LogicalExpression":
217
253
  leftStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.left);
218
- rightStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.right);
254
+ rightStyleObjectExpression =
255
+ astHelpers.getStyleObjectExpressionFromNode(node.right);
219
256
  return []
220
257
  .concat(leftStyleObjectExpression)
221
258
  .concat(rightStyleObjectExpression);
222
- case 'ConditionalExpression':
259
+ case "ConditionalExpression":
223
260
  leftStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.consequent);
224
- rightStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.alternate);
261
+ rightStyleObjectExpression =
262
+ astHelpers.getStyleObjectExpressionFromNode(node.alternate);
225
263
  return []
226
264
  .concat(leftStyleObjectExpression)
227
265
  .concat(rightStyleObjectExpression);
@@ -235,27 +273,31 @@ export const astHelpers = {
235
273
  if (!node) {
236
274
  return [];
237
275
  }
238
- if (node.type === 'ObjectExpression') {
276
+ if (node.type === "ObjectExpression") {
239
277
  return [astHelpers.getColorLiteralsFromExpression(node)];
240
278
  }
241
279
  switch (node.type) {
242
- case 'LogicalExpression':
280
+ case "LogicalExpression":
243
281
  leftColorLiterals = astHelpers.getColorLiteralsFromNode(node.left);
244
282
  rightColorLiterals = astHelpers.getColorLiteralsFromNode(node.right);
245
- return [].concat(leftColorLiterals).concat(rightColorLiterals);
246
- case 'ConditionalExpression':
283
+ return []
284
+ .concat(leftColorLiterals)
285
+ .concat(rightColorLiterals);
286
+ case "ConditionalExpression":
247
287
  leftColorLiterals = astHelpers.getColorLiteralsFromNode(node.consequent);
248
288
  rightColorLiterals = astHelpers.getColorLiteralsFromNode(node.alternate);
249
- return [].concat(leftColorLiterals).concat(rightColorLiterals);
289
+ return []
290
+ .concat(leftColorLiterals)
291
+ .concat(rightColorLiterals);
250
292
  default:
251
293
  return [];
252
294
  }
253
295
  },
254
296
  hasArrayOfStyleReferences: function (node) {
255
297
  return (node &&
256
- Boolean(node.type === 'JSXExpressionContainer' &&
298
+ Boolean(node.type === "JSXExpressionContainer" &&
257
299
  node.expression &&
258
- node.expression.type === 'ArrayExpression'));
300
+ node.expression.type === "ArrayExpression"));
259
301
  },
260
302
  getStyleReferenceFromExpression: function (node) {
261
303
  const result = [];
@@ -267,7 +309,7 @@ export const astHelpers = {
267
309
  if (property) {
268
310
  result.push(property);
269
311
  }
270
- return result.join('.');
312
+ return result.join(".");
271
313
  },
272
314
  getStyleObjectFromExpression: function (node) {
273
315
  const obj = {};
@@ -277,27 +319,27 @@ export const astHelpers = {
277
319
  if (!p.value || !p.key) {
278
320
  return;
279
321
  }
280
- if (p.value.type === 'Literal') {
322
+ if (p.value.type === "Literal") {
281
323
  invalid = true;
282
324
  obj[p.key.name] = p.value.value;
283
325
  }
284
- else if (p.value.type === 'ConditionalExpression') {
326
+ else if (p.value.type === "ConditionalExpression") {
285
327
  const innerNode = p.value;
286
- if (innerNode.consequent.type === 'Literal' ||
287
- innerNode.alternate.type === 'Literal') {
328
+ if (innerNode.consequent.type === "Literal" ||
329
+ innerNode.alternate.type === "Literal") {
288
330
  invalid = true;
289
331
  obj[p.key.name] = getSourceCode(innerNode);
290
332
  }
291
333
  }
292
- else if (p.value.type === 'UnaryExpression' &&
293
- p.value.operator === '-' &&
294
- p.value.argument.type === 'Literal') {
334
+ else if (p.value.type === "UnaryExpression" &&
335
+ p.value.operator === "-" &&
336
+ p.value.argument.type === "Literal") {
295
337
  invalid = true;
296
338
  obj[p.key.name] = -1 * p.value.argument.value;
297
339
  }
298
- else if (p.value.type === 'UnaryExpression' &&
299
- p.value.operator === '+' &&
300
- p.value.argument.type === 'Literal') {
340
+ else if (p.value.type === "UnaryExpression" &&
341
+ p.value.operator === "+" &&
342
+ p.value.argument.type === "Literal") {
301
343
  invalid = true;
302
344
  obj[p.key.name] = p.value.argument.value;
303
345
  }
@@ -312,15 +354,15 @@ export const astHelpers = {
312
354
  node.properties.forEach((p) => {
313
355
  if (p.key &&
314
356
  p.key.name &&
315
- p.key.name.toLowerCase().indexOf('color') !== -1) {
316
- if (p.value.type === 'Literal') {
357
+ p.key.name.toLowerCase().indexOf("color") !== -1) {
358
+ if (p.value.type === "Literal") {
317
359
  invalid = true;
318
360
  obj[p.key.name] = p.value.value;
319
361
  }
320
- else if (p.value.type === 'ConditionalExpression') {
362
+ else if (p.value.type === "ConditionalExpression") {
321
363
  const innerNode = p.value;
322
- if (innerNode.consequent.type === 'Literal' ||
323
- innerNode.alternate.type === 'Literal') {
364
+ if (innerNode.consequent.type === "Literal" ||
365
+ innerNode.alternate.type === "Literal") {
324
366
  invalid = true;
325
367
  obj[p.key.name] = getSourceCode(innerNode);
326
368
  }
@@ -343,17 +385,17 @@ export const astHelpers = {
343
385
  getPotentialStyleReferenceFromMemberExpression: function (node) {
344
386
  if (node &&
345
387
  node.object &&
346
- node.object.type === 'Identifier' &&
388
+ node.object.type === "Identifier" &&
347
389
  node.object.name &&
348
390
  node.property &&
349
- node.property.type === 'Identifier' &&
391
+ node.property.type === "Identifier" &&
350
392
  node.property.name &&
351
- node.parent.type !== 'MemberExpression') {
352
- return [node.object.name, node.property.name].join('.');
393
+ node.parent.type !== "MemberExpression") {
394
+ return [node.object.name, node.property.name].join(".");
353
395
  }
354
396
  },
355
397
  isEitherShortHand: function (property1, property2) {
356
- const shorthands = ['margin', 'padding', 'border', 'flex'];
398
+ const shorthands = ["margin", "padding", "border", "flex"];
357
399
  if (shorthands.includes(property1)) {
358
400
  return property2.startsWith(property1);
359
401
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-native",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "React Native specific linting rules for Oxlint",
5
5
  "keywords": [
6
6
  "oxlint",
@@ -32,7 +32,8 @@
32
32
  "format": "oxfmt",
33
33
  "format:check": "oxfmt --check",
34
34
  "test": "jest",
35
- "test:watch": "jest --watch"
35
+ "test:watch": "jest --watch",
36
+ "release": "semantic-release"
36
37
  },
37
38
  "dependencies": {
38
39
  "@oxlint/plugins": "1.43.0"
@@ -41,15 +42,26 @@
41
42
  "@babel/core": "7.29.0",
42
43
  "@babel/preset-env": "7.29.0",
43
44
  "@babel/preset-typescript": "7.28.5",
45
+ "@commitlint/cli": "^20.4.1",
46
+ "@commitlint/config-conventional": "^20.4.1",
47
+ "@semantic-release/changelog": "^6.0.3",
48
+ "@semantic-release/git": "^10.0.1",
44
49
  "@types/jest": "30.0.0",
45
- "@types/node": "25.2.0",
50
+ "@types/node": "25.2.3",
51
+ "commitlint": "^20.4.1",
46
52
  "jest": "30.2.0",
47
- "oxfmt": "^0.28.0",
48
- "oxlint": "1.43.0",
49
- "oxlint-tsgolint": "^0.11.4",
53
+ "oxfmt": "^0.31.0",
54
+ "oxlint": "1.46.0",
55
+ "oxlint-tsgolint": "^0.12.0",
56
+ "semantic-release": "^25.0.3",
50
57
  "typescript": "5.9.3"
51
58
  },
52
59
  "peerDependencies": {
53
60
  "oxlint": ">=1.0.0"
61
+ },
62
+ "commitlint": {
63
+ "extends": [
64
+ "@commitlint/config-conventional"
65
+ ]
54
66
  }
55
67
  }