@hypersolid-dev/eslint-config 5.0.0 → 6.0.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +17 -1
  3. package/dist/base.js +14 -1
  4. package/dist/jest.d.ts +3 -0
  5. package/dist/jest.js +56 -0
  6. package/dist/plugins/props-destructuring.js +12 -12
  7. package/dist/plugins/react-native-a11y/rules/accessibility-actions-need-handler.js +2 -2
  8. package/dist/plugins/react-native-a11y/rules/accessibility-hint.js +2 -2
  9. package/dist/plugins/react-native-a11y/rules/accessibility-label.js +2 -2
  10. package/dist/plugins/react-native-a11y/rules/accessibility-role.js +2 -2
  11. package/dist/plugins/react-native-a11y/rules/accessibility-value-needs-range.js +25 -27
  12. package/dist/plugins/react-native-a11y/rules/adjustable-needs-value.js +16 -18
  13. package/dist/plugins/react-native-a11y/rules/gate-announce-for-accessibility.js +4 -4
  14. package/dist/plugins/react-native-a11y/rules/input-purpose.js +2 -2
  15. package/dist/plugins/react-native-a11y/rules/no-aria-legacy-mix.js +20 -22
  16. package/dist/plugins/react-native-a11y/rules/no-deprecated-accessibility-focus.js +12 -14
  17. package/dist/plugins/react-native-a11y/rules/no-dropped-aria-on-text.js +2 -2
  18. package/dist/plugins/react-native-a11y/rules/no-font-scaling-disabled.js +24 -26
  19. package/dist/plugins/react-native-a11y/rules/no-ineffective-accessibility-props.js +34 -36
  20. package/dist/plugins/react-native-a11y/rules/no-invalid-accessibility-state.js +31 -33
  21. package/dist/plugins/react-native-a11y/rules/no-nested-focus-stops.js +2 -2
  22. package/dist/plugins/react-native-a11y/rules/no-press-in-activation.js +2 -2
  23. package/dist/plugins/react-native-a11y/rules/no-unnamed-interactive.js +2 -2
  24. package/dist/plugins/react-native-a11y/rules/require-invert-colors-opt-out.js +2 -2
  25. package/dist/plugins/react-native-a11y/rules/require-modal-a11y-on-sheets.js +2 -2
  26. package/dist/plugins/react-native-a11y/utils/get-amount-of-text-children.js +15 -13
  27. package/dist/plugins/rules/boolean-guard.js +3 -3
  28. package/dist/plugins/rules/comment-style.js +43 -34
  29. package/dist/plugins/rules/effect-takes-callback.js +1 -1
  30. package/dist/plugins/rules/jsdoc-for-props.js +1 -1
  31. package/dist/plugins/rules/no-callback-payload.js +18 -15
  32. package/dist/plugins/rules/no-markup-variable.js +41 -35
  33. package/dist/plugins/rules/no-unnecessary-use-callback.js +2 -2
  34. package/dist/plugins/rules/regex-pattern-const.js +1 -1
  35. package/dist/plugins/rules/test-id-default.js +23 -27
  36. package/dist/plugins/rules/test-id-format.js +14 -18
  37. package/dist/plugins/rules/test-id-from-props.js +23 -27
  38. package/dist/plugins/rules/undefined-check.js +2 -2
  39. package/dist/plugins/rules/webview-allow-list.js +22 -26
  40. package/dist/plugins/utils/expressions.d.ts +3 -3
  41. package/dist/plugins/utils/expressions.js +94 -78
  42. package/dist/plugins/utils/types.js +14 -12
  43. package/dist/react-native.js +28 -0
  44. package/dist/shared.d.ts +3 -3
  45. package/dist/shared.js +168 -112
  46. package/package.json +12 -2
@@ -31,46 +31,44 @@ const noIneffectiveAccessibilityProps = createRule({
31
31
  },
32
32
  schema: [],
33
33
  },
34
- create(context) {
35
- return {
36
- JSXOpeningElement(node) {
37
- const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
38
- if (role === "listitem") {
39
- context.report({ node, messageId: "listItemRole" });
34
+ create: (context) => ({
35
+ JSXOpeningElement: (node) => {
36
+ const role = getLiteralPropertyValue(node, ["accessibilityRole", "role"]);
37
+ if (role === "listitem") {
38
+ context.report({ node, messageId: "listItemRole" });
39
+ }
40
+ if (role === "alert") {
41
+ context.report({ node, messageId: "alertRole" });
42
+ }
43
+ const isHidden = isDefinitelyHidden(node);
44
+ if (isHidden && hasLabelProperty(node)) {
45
+ context.report({ node, messageId: "hiddenWithLabel" });
46
+ }
47
+ if (getLiteralPropertyValue(node, "accessible") === false) {
48
+ const name = getElementName(node);
49
+ if (isHidden) {
50
+ context.report({ node, messageId: "hiddenWithAccessibleFalse" });
40
51
  }
41
- if (role === "alert") {
42
- context.report({ node, messageId: "alertRole" });
43
- }
44
- const isHidden = isDefinitelyHidden(node);
45
- if (isHidden && hasLabelProperty(node)) {
46
- context.report({ node, messageId: "hiddenWithLabel" });
47
- }
48
- if (getLiteralPropertyValue(node, "accessible") === false) {
49
- const name = getElementName(node);
50
- if (isHidden) {
51
- context.report({ node, messageId: "hiddenWithAccessibleFalse" });
52
- }
53
- else if (name !== undefined &&
54
- DEFAULTS_TO_INACCESSIBLE.has(name) &&
55
- // A spread may carry the `accessible` this one is overriding.
56
- !hasSpreadProps(node)) {
57
- context.report({
58
- node,
59
- messageId: "accessibleFalseIsDefault",
60
- data: { name },
61
- });
62
- }
63
- }
64
- const labelledBy = getProperty(node, LABELLED_BY_PROPS);
65
- if (labelledBy && !hasLabelProperty(node)) {
52
+ else if (name !== undefined &&
53
+ DEFAULTS_TO_INACCESSIBLE.has(name) &&
54
+ // A spread may carry the `accessible` this one is overriding.
55
+ !hasSpreadProps(node)) {
66
56
  context.report({
67
57
  node,
68
- messageId: "labelledByWithoutLabel",
69
- data: { prop: labelledBy.name.name },
58
+ messageId: "accessibleFalseIsDefault",
59
+ data: { name },
70
60
  });
71
61
  }
72
- },
73
- };
74
- },
62
+ }
63
+ const labelledBy = getProperty(node, LABELLED_BY_PROPS);
64
+ if (labelledBy && !hasLabelProperty(node)) {
65
+ context.report({
66
+ node,
67
+ messageId: "labelledByWithoutLabel",
68
+ data: { prop: labelledBy.name.name },
69
+ });
70
+ }
71
+ },
72
+ }),
75
73
  });
76
74
  export default noIneffectiveAccessibilityProps;
@@ -44,39 +44,37 @@ const noInvalidAccessibilityState = createRule({
44
44
  },
45
45
  schema: [],
46
46
  },
47
- create(context) {
48
- return {
49
- JSXOpeningElement(node) {
50
- const attribute = getProperty(node, "accessibilityState");
51
- if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
52
- return;
47
+ create: (context) => ({
48
+ JSXOpeningElement: (node) => {
49
+ const attribute = getProperty(node, "accessibilityState");
50
+ if (attribute?.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
51
+ return;
52
+ }
53
+ const { expression } = attribute.value;
54
+ // An object literal here is already rejected by the React Native types
55
+ if (expression.type !== AST_NODE_TYPES.Identifier) {
56
+ return;
57
+ }
58
+ const resolved = resolveObjectBinding(context.sourceCode.getScope(expression), expression);
59
+ if (!resolved) {
60
+ return;
61
+ }
62
+ for (const property of resolved.properties) {
63
+ if (property.type !== AST_NODE_TYPES.Property ||
64
+ property.key.type !== AST_NODE_TYPES.Identifier ||
65
+ ACCESSIBILITY_STATE_KEYS.includes(property.key.name)) {
66
+ continue;
53
67
  }
54
- const { expression } = attribute.value;
55
- // An object literal here is already rejected by the React Native types
56
- if (expression.type !== AST_NODE_TYPES.Identifier) {
57
- return;
58
- }
59
- const resolved = resolveObjectBinding(context.sourceCode.getScope(expression), expression);
60
- if (!resolved) {
61
- return;
62
- }
63
- for (const property of resolved.properties) {
64
- if (property.type !== AST_NODE_TYPES.Property ||
65
- property.key.type !== AST_NODE_TYPES.Identifier ||
66
- ACCESSIBILITY_STATE_KEYS.includes(property.key.name)) {
67
- continue;
68
- }
69
- context.report({
70
- node: property,
71
- messageId: "invalidStateKey",
72
- data: {
73
- key: property.key.name,
74
- supported: ACCESSIBILITY_STATE_KEYS.join(", "),
75
- },
76
- });
77
- }
78
- },
79
- };
80
- },
68
+ context.report({
69
+ node: property,
70
+ messageId: "invalidStateKey",
71
+ data: {
72
+ key: property.key.name,
73
+ supported: ACCESSIBILITY_STATE_KEYS.join(", "),
74
+ },
75
+ });
76
+ }
77
+ },
78
+ }),
81
79
  });
82
80
  export default noInvalidAccessibilityState;
@@ -30,7 +30,7 @@ const noNestedFocusStops = createRule({
30
30
  },
31
31
  ],
32
32
  },
33
- create(context) {
33
+ create: (context) => {
34
34
  // The touchables plus whatever this project wraps them in
35
35
  const components = getComponents({
36
36
  context,
@@ -44,7 +44,7 @@ const noNestedFocusStops = createRule({
44
44
  hasTouchHandler(node));
45
45
  };
46
46
  return {
47
- JSXOpeningElement(node) {
47
+ JSXOpeningElement: (node) => {
48
48
  const componentName = getElementName(node);
49
49
  if (componentName === undefined ||
50
50
  !isDefinitelyGrouped(node) ||
@@ -29,13 +29,13 @@ const noPressInActivation = createRule({
29
29
  },
30
30
  ],
31
31
  },
32
- create(context) {
32
+ create: (context) => {
33
33
  const components = getComponents({
34
34
  context,
35
35
  defaultComponents: TOUCHABLES,
36
36
  });
37
37
  return {
38
- JSXOpeningElement(node) {
38
+ JSXOpeningElement: (node) => {
39
39
  const componentName = getElementName(node);
40
40
  if (componentName === undefined) {
41
41
  return;
@@ -38,10 +38,10 @@ const noUnnamedInteractive = createRule({
38
38
  },
39
39
  ],
40
40
  },
41
- create(context) {
41
+ create: (context) => {
42
42
  const components = getComponents({ context });
43
43
  return {
44
- JSXOpeningElement(node) {
44
+ JSXOpeningElement: (node) => {
45
45
  const componentName = getElementName(node);
46
46
  if (componentName === undefined) {
47
47
  return;
@@ -29,7 +29,7 @@ const requireInvertColorsOptOut = createRule({
29
29
  },
30
30
  ],
31
31
  },
32
- create(context) {
32
+ create: (context) => {
33
33
  /**
34
34
  * Matched on the element name, so an image from expo-image or a wrapper
35
35
  * named through additionalComponents is checked the same way. Deliberately
@@ -41,7 +41,7 @@ const requireInvertColorsOptOut = createRule({
41
41
  defaultComponents: IMAGES,
42
42
  });
43
43
  return {
44
- JSXOpeningElement(node) {
44
+ JSXOpeningElement: (node) => {
45
45
  const componentName = getElementName(node);
46
46
  if (componentName === undefined) {
47
47
  return;
@@ -27,7 +27,7 @@ const requireModalA11yOnSheets = createRule({
27
27
  },
28
28
  ],
29
29
  },
30
- create(context) {
30
+ create: (context) => {
31
31
  /**
32
32
  * No default list: a sheet is whatever this project calls one, and
33
33
  * hardcoding one library's component names would fire on a codebase that
@@ -35,7 +35,7 @@ const requireModalA11yOnSheets = createRule({
35
35
  */
36
36
  const components = getComponents({ context });
37
37
  return {
38
- JSXOpeningElement(node) {
38
+ JSXOpeningElement: (node) => {
39
39
  const componentName = getElementName(node);
40
40
  if (componentName === undefined) {
41
41
  return;
@@ -32,6 +32,20 @@ const branchesOf = (expression) => {
32
32
  };
33
33
  const isJsxChild = (expression) => expression.type === AST_NODE_TYPES.JSXElement ||
34
34
  expression.type === AST_NODE_TYPES.JSXFragment;
35
+ const countExpressionText = (expression, amount) => {
36
+ if (expression.type === AST_NODE_TYPES.Literal ||
37
+ expression.type === AST_NODE_TYPES.TemplateLiteral) {
38
+ return amount + 1;
39
+ }
40
+ /**
41
+ * JSX inside an expression renders like any other child, and a branch
42
+ * that renders text still names the control
43
+ */
44
+ const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
45
+ return rendered.length > 0
46
+ ? getAmountOfTextChildren(rendered, amount)
47
+ : amount;
48
+ };
35
49
  /**
36
50
  * Counts children that render visible text, which is what makes an
37
51
  * `accessibilityLabel` redundant. Whitespace between JSX tags is not text, and
@@ -42,19 +56,7 @@ const getAmountOfTextChildren = (children, oldAmount = 0) => children.reduce((am
42
56
  return child.value.trim().length > 0 ? amount + 1 : amount;
43
57
  }
44
58
  if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
45
- const { expression } = child;
46
- if (expression.type === AST_NODE_TYPES.Literal ||
47
- expression.type === AST_NODE_TYPES.TemplateLiteral) {
48
- return amount + 1;
49
- }
50
- /**
51
- * JSX inside an expression renders like any other child, and a branch
52
- * that renders text still names the control
53
- */
54
- const rendered = (isJsxChild(expression) ? [expression] : branchesOf(expression)).filter(isJsxChild);
55
- return rendered.length > 0
56
- ? getAmountOfTextChildren(rendered, amount)
57
- : amount;
59
+ return countExpressionText(child.expression, amount);
58
60
  }
59
61
  if (child.type === AST_NODE_TYPES.JSXFragment) {
60
62
  return getAmountOfTextChildren(child.children, amount);
@@ -69,7 +69,7 @@ const booleanGuard = createRule({
69
69
  preferBangBang: "`!== undefined` is `!!` for this type. Write `!!{{condition}} && …`.",
70
70
  },
71
71
  },
72
- create(context) {
72
+ create: (context) => {
73
73
  const services = ESLintUtils.getParserServices(context, true);
74
74
  if (!services.program)
75
75
  return {};
@@ -99,7 +99,7 @@ const booleanGuard = createRule({
99
99
  return reads.every((reference) => isInRenderPosition(reference.identifier));
100
100
  };
101
101
  return {
102
- ConditionalExpression(node) {
102
+ ConditionalExpression: (node) => {
103
103
  // The shape is what proves a `const` holding this is a guard.
104
104
  const isConsequentGuarded = isRenderable(node.consequent) && isNothing(node.alternate);
105
105
  const isAlternateGuarded = isRenderable(node.alternate) && isNothing(node.consequent);
@@ -122,7 +122,7 @@ const booleanGuard = createRule({
122
122
  data: { condition: isAlternateGuarded ? `!${nameOf(node.test)}` : nameOf(node.test) },
123
123
  });
124
124
  },
125
- LogicalExpression(node) {
125
+ LogicalExpression: (node) => {
126
126
  if (node.operator !== "&&")
127
127
  return;
128
128
  if (!isInRenderPosition(node))
@@ -74,7 +74,7 @@ export default createRule({
74
74
  tooLong: "This comment line is {{length}} characters. Keep it to {{max}}.",
75
75
  },
76
76
  },
77
- create(context) {
77
+ create: (context) => {
78
78
  const { sourceCode } = context;
79
79
  const maxLength = context.options[0]?.maxLength;
80
80
  const isTooLong = (comment) => Boolean(maxLength) && lineLength(comment) > (maxLength ?? 0);
@@ -86,7 +86,7 @@ export default createRule({
86
86
  });
87
87
  };
88
88
  return {
89
- "Program:exit"() {
89
+ "Program:exit": () => {
90
90
  let run = [];
91
91
  const doesContinue = (comment) => {
92
92
  const last = run.at(-1);
@@ -108,48 +108,57 @@ export default createRule({
108
108
  }
109
109
  run = [];
110
110
  };
111
- for (const comment of sourceCode.getAllComments()) {
112
- if (comment.type === AST_TOKEN_TYPES.Block) {
113
- const rows = comment.loc.end.line - comment.loc.start.line + 1;
114
- // A JSX comment's `{` is syntax, so a row holding only `{/* … */}` starts its row.
115
- const isOwnRow = isRowStart(sourceCode, comment) ||
116
- (isJsxComment(sourceCode, comment) &&
117
- !sourceCode.lines[comment.loc.start.line - 1]
118
- ?.slice(0, Math.max(0, comment.loc.start.column - 1))
119
- .trim());
120
- if (!isJsdoc(comment) && rows === 1 && isOwnRow) {
121
- if (!doesContinue(comment))
122
- flush();
123
- run.push(comment);
124
- continue;
125
- }
126
- flush();
127
- if (isJsdoc(comment)) {
128
- if (isTooLong(comment))
129
- reportTooLong(comment);
130
- continue;
131
- }
132
- if (rows > 1) {
133
- context.report({
134
- loc: comment.loc,
135
- messageId: isJsxComment(sourceCode, comment) ? "jsxBlock" : "blockRun",
136
- data: { rows: String(rows) },
137
- });
138
- }
139
- else if (isTooLong(comment)) {
111
+ /** A JSX comment's `{` is syntax, so a row holding only a braced JSX comment starts its row. */
112
+ const isOwnRow = (comment) => isRowStart(sourceCode, comment) ||
113
+ (isJsxComment(sourceCode, comment) &&
114
+ !sourceCode.lines[comment.loc.start.line - 1]
115
+ ?.slice(0, Math.max(0, comment.loc.start.column - 1))
116
+ .trim());
117
+ /** A block comment that is not part of a run: JSDoc, multi-row, or sharing its row with code. */
118
+ const checkLoneBlock = (comment, rows) => {
119
+ if (isJsdoc(comment)) {
120
+ if (isTooLong(comment))
140
121
  reportTooLong(comment);
141
- }
142
- continue;
122
+ return;
123
+ }
124
+ if (rows > 1) {
125
+ context.report({
126
+ loc: comment.loc,
127
+ messageId: isJsxComment(sourceCode, comment) ? "jsxBlock" : "blockRun",
128
+ data: { rows: String(rows) },
129
+ });
143
130
  }
131
+ else if (isTooLong(comment)) {
132
+ reportTooLong(comment);
133
+ }
134
+ };
135
+ const visitBlock = (comment) => {
136
+ const rows = comment.loc.end.line - comment.loc.start.line + 1;
137
+ if (!isJsdoc(comment) && rows === 1 && isOwnRow(comment)) {
138
+ if (!doesContinue(comment))
139
+ flush();
140
+ run.push(comment);
141
+ return;
142
+ }
143
+ flush();
144
+ checkLoneBlock(comment, rows);
145
+ };
146
+ const visitLine = (comment) => {
144
147
  if (!doesContinue(comment))
145
148
  flush();
146
149
  if (!isRowStart(sourceCode, comment)) {
147
150
  flush();
148
151
  if (isTooLong(comment))
149
152
  reportTooLong(comment);
150
- continue;
153
+ return;
151
154
  }
152
155
  run.push(comment);
156
+ };
157
+ for (const comment of sourceCode.getAllComments()) {
158
+ if (comment.type === AST_TOKEN_TYPES.Block)
159
+ visitBlock(comment);
160
+ else
161
+ visitLine(comment);
153
162
  }
154
163
  flush();
155
164
  },
@@ -48,7 +48,7 @@ const effectTakesCallback = createRule({
48
48
  callbackFirst: "`{{name}}` ends in Effect but its first parameter is not a callback. Take the callback first, or drop the Effect suffix.",
49
49
  },
50
50
  },
51
- create(context) {
51
+ create: (context) => {
52
52
  const services = context.sourceCode.parserServices;
53
53
  const checker = services?.program ? services.program.getTypeChecker() : undefined;
54
54
  /** Whether a value of this type can be called, once `undefined` is set aside. */
@@ -28,7 +28,7 @@ export default createRule({
28
28
  form: "Write this as a JSDoc block, not `{{ form }}`. A member's comment is what an editor shows when someone hovers it elsewhere, and only JSDoc reaches there.",
29
29
  },
30
30
  },
31
- create(context) {
31
+ create: (context) => {
32
32
  const { sourceCode } = context;
33
33
  const check = (node) => {
34
34
  // A directive neither documents the member nor separates it from the comment above.
@@ -2,6 +2,18 @@ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2
2
  import createRule from "../utils/create-rule.js";
3
3
  import { componentName, isWrapperCall } from "../utils/component-name.js";
4
4
  const CALLBACK_PATTERN = /^on[A-Z]/;
5
+ /** Adds each identifier an object pattern binds to `props`, and its rest binding to `objects` too. */
6
+ const addPatternBindings = (pattern, props, objects) => {
7
+ for (const property of pattern.properties) {
8
+ const value = property.type === AST_NODE_TYPES.RestElement ? property.argument : property.value;
9
+ const binding = value.type === AST_NODE_TYPES.AssignmentPattern ? value.left : value;
10
+ if (binding.type !== AST_NODE_TYPES.Identifier)
11
+ continue;
12
+ props.add(binding);
13
+ if (property.type === AST_NODE_TYPES.RestElement)
14
+ objects.add(binding);
15
+ }
16
+ };
5
17
  /** The first parameter's bindings: each prop it destructures, and the props object itself or a rest of it. */
6
18
  const propertyBindings = (parameter) => {
7
19
  const props = new Set();
@@ -11,17 +23,8 @@ const propertyBindings = (parameter) => {
11
23
  props.add(target);
12
24
  objects.add(target);
13
25
  }
14
- if (target?.type === AST_NODE_TYPES.ObjectPattern) {
15
- for (const property of target.properties) {
16
- const value = property.type === AST_NODE_TYPES.RestElement ? property.argument : property.value;
17
- const binding = value.type === AST_NODE_TYPES.AssignmentPattern ? value.left : value;
18
- if (binding.type !== AST_NODE_TYPES.Identifier)
19
- continue;
20
- props.add(binding);
21
- if (property.type === AST_NODE_TYPES.RestElement)
22
- objects.add(binding);
23
- }
24
- }
26
+ if (target?.type === AST_NODE_TYPES.ObjectPattern)
27
+ addPatternBindings(target, props, objects);
25
28
  return { props, objects };
26
29
  };
27
30
  /**
@@ -53,7 +56,7 @@ const noCallbackPayload = createRule({
53
56
  payload: "`{{ callback }}` is called with values `{{ component }}` only received from its parent. Let the parent bind them where it passes `{{ callback }}`, and call it with what this component itself knows (or nothing), so the component stays free of the payload's domain shape.",
54
57
  },
55
58
  },
56
- create(context) {
59
+ create: (context) => {
57
60
  const { sourceCode } = context;
58
61
  const exported = new Set();
59
62
  const pending = [];
@@ -173,7 +176,7 @@ const noCallbackPayload = createRule({
173
176
  ArrowFunctionExpression: check,
174
177
  FunctionDeclaration: check,
175
178
  FunctionExpression: check,
176
- ExportDefaultDeclaration(node) {
179
+ ExportDefaultDeclaration: (node) => {
177
180
  // `export default memo(Row)` exports `Row`, however many wrappers it is under.
178
181
  let declaration = node.declaration;
179
182
  while (isWrapperCall(declaration))
@@ -187,7 +190,7 @@ const noCallbackPayload = createRule({
187
190
  exported.add(declaration.id.name);
188
191
  }
189
192
  },
190
- ExportNamedDeclaration(node) {
193
+ ExportNamedDeclaration: (node) => {
191
194
  for (const specifier of node.specifiers) {
192
195
  if (specifier.local.type === AST_NODE_TYPES.Identifier)
193
196
  exported.add(specifier.local.name);
@@ -202,7 +205,7 @@ const noCallbackPayload = createRule({
202
205
  }
203
206
  }
204
207
  },
205
- "Program:exit"() {
208
+ "Program:exit": () => {
206
209
  for (const { node, data } of pending) {
207
210
  if (exported.has(data.component))
208
211
  context.report({ node, messageId: "payload", data });
@@ -63,7 +63,45 @@ const noMarkupVariable = createRule({
63
63
  inline: "`{{ name }}` holds JSX that `{{ component }}` reads only once. Write it where it is rendered.",
64
64
  },
65
65
  },
66
- create(context) {
66
+ create: (context) => {
67
+ /** Read once, never reassigned, and that read is a JSX child rather than an attribute value. */
68
+ const isRenderedOnceInline = (declarator) => {
69
+ const variable = context.sourceCode.getDeclaredVariables(declarator).at(0);
70
+ const references = variable?.references ?? [];
71
+ const reads = references.filter((ref) => ref.isRead());
72
+ const isReassigned = references.some((ref) => ref.isWrite() && ref.identifier !== declarator.id);
73
+ const container = reads.at(0)?.identifier.parent;
74
+ return (reads.length === 1 &&
75
+ !isReassigned &&
76
+ container?.type === AST_NODE_TYPES.JSXExpressionContainer &&
77
+ container.parent.type !== AST_NODE_TYPES.JSXAttribute);
78
+ };
79
+ const checkDeclarator = (declarator, component) => {
80
+ if (declarator.id.type !== AST_NODE_TYPES.Identifier)
81
+ return;
82
+ const jsx = drawnJsx(declarator.init);
83
+ if (jsx.length === 0)
84
+ return;
85
+ if (jsx.some(hasStyledElement)) {
86
+ context.report({
87
+ node: declarator.id,
88
+ messageId: "extract",
89
+ data: {
90
+ name: declarator.id.name,
91
+ component,
92
+ suggestion: `${component}${toComponentName(declarator.id.name)}`,
93
+ },
94
+ });
95
+ return;
96
+ }
97
+ if (isRenderedOnceInline(declarator)) {
98
+ context.report({
99
+ node: declarator.id,
100
+ messageId: "inline",
101
+ data: { name: declarator.id.name, component },
102
+ });
103
+ }
104
+ };
67
105
  const check = (fn) => {
68
106
  const component = componentName(fn);
69
107
  if (!component || fn.body.type !== AST_NODE_TYPES.BlockStatement)
@@ -71,40 +109,8 @@ const noMarkupVariable = createRule({
71
109
  for (const statement of fn.body.body) {
72
110
  if (statement.type !== AST_NODE_TYPES.VariableDeclaration)
73
111
  continue;
74
- for (const declarator of statement.declarations) {
75
- if (declarator.id.type !== AST_NODE_TYPES.Identifier)
76
- continue;
77
- const jsx = drawnJsx(declarator.init);
78
- if (jsx.length === 0)
79
- continue;
80
- if (!jsx.some(hasStyledElement)) {
81
- const variable = context.sourceCode.getDeclaredVariables(declarator).at(0);
82
- const references = variable?.references ?? [];
83
- const reads = references.filter((ref) => ref.isRead());
84
- const isReassigned = references.some((ref) => ref.isWrite() && ref.identifier !== declarator.id);
85
- const container = reads.at(0)?.identifier.parent;
86
- if (reads.length === 1 &&
87
- !isReassigned &&
88
- container?.type === AST_NODE_TYPES.JSXExpressionContainer &&
89
- container.parent.type !== AST_NODE_TYPES.JSXAttribute) {
90
- context.report({
91
- node: declarator.id,
92
- messageId: "inline",
93
- data: { name: declarator.id.name, component },
94
- });
95
- }
96
- continue;
97
- }
98
- context.report({
99
- node: declarator.id,
100
- messageId: "extract",
101
- data: {
102
- name: declarator.id.name,
103
- component,
104
- suggestion: `${component}${toComponentName(declarator.id.name)}`,
105
- },
106
- });
107
- }
112
+ for (const declarator of statement.declarations)
113
+ checkDeclarator(declarator, component);
108
114
  }
109
115
  };
110
116
  return {
@@ -84,10 +84,10 @@ const noUnnecessaryUseCallback = createRule({
84
84
  unnecessary: "This callback captures no render-local values. Use an existing function or define it at module scope, unless a proven per-instance identity contract requires memoization; explain that contract in a scoped suppression.",
85
85
  },
86
86
  },
87
- create(context) {
87
+ create: (context) => {
88
88
  const { sourceCode } = context;
89
89
  return {
90
- CallExpression(node) {
90
+ CallExpression: (node) => {
91
91
  if (!isUseCallback(sourceCode, node.callee))
92
92
  return;
93
93
  const [callback, dependencies] = [node.arguments.at(0), node.arguments.at(1)];
@@ -64,7 +64,7 @@ export default createRule({
64
64
  rename: "Rename `{{name}}` to end in `_PATTERN` (SCREAMING_SNAKE_CASE), so the name says it is a regular expression.",
65
65
  },
66
66
  },
67
- create(context) {
67
+ create: (context) => {
68
68
  const check = (node) => {
69
69
  if (!isRegex(node))
70
70
  return;