eslint-plugin-absolute 0.2.0 → 0.2.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.
@@ -20,37 +20,20 @@ type CandidateVariable = {
20
20
  };
21
21
 
22
22
  export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
23
- meta: {
24
- type: "suggestion",
25
- docs: {
26
- description:
27
- "Disallow variables that are only passed to a single custom child component. For useState, only report if both the state and its setter are exclusively passed to a single custom child. For general variables, only report if a given child receives exactly one such candidate – if two or more are passed to the same component type, they’re assumed to be settings that belong on the parent."
28
- },
29
- schema: [],
30
- messages: {
31
- stateAndSetterToChild:
32
- "State variable '{{stateVarName}}' and its setter '{{setterVarName}}' are only passed to a single custom child component. Consider moving the state into that component.",
33
- variableToChild:
34
- "Variable '{{varName}}' is only passed to a single custom child component. Consider moving it to that component."
35
- }
36
- },
37
-
38
- defaultOptions: [],
39
-
40
23
  create(context) {
41
24
  // A list of candidate variables for reporting (for general variables only).
42
25
  const candidateVariables: CandidateVariable[] = [];
43
26
 
44
- function getSingleSetElement<T>(set: Set<T>): T | null {
27
+ const getSingleSetElement = <T>(set: Set<T>) => {
45
28
  for (const value of set) {
46
29
  return value;
47
30
  }
48
31
  return null;
49
- }
32
+ };
50
33
 
51
- function getRightmostJSXIdentifier(
34
+ const getRightmostJSXIdentifier = (
52
35
  name: TSESTree.JSXTagNameExpression
53
- ): TSESTree.JSXIdentifier | null {
36
+ ) => {
54
37
  let current: TSESTree.JSXTagNameExpression = name;
55
38
  while (current.type === AST_NODE_TYPES.JSXMemberExpression) {
56
39
  current = current.property;
@@ -59,11 +42,11 @@ export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
59
42
  return current;
60
43
  }
61
44
  return null;
62
- }
45
+ };
63
46
 
64
- function getLeftmostJSXIdentifier(
47
+ const getLeftmostJSXIdentifier = (
65
48
  name: TSESTree.JSXTagNameExpression
66
- ): TSESTree.JSXIdentifier | null {
49
+ ) => {
67
50
  let current: TSESTree.JSXTagNameExpression = name;
68
51
  while (current.type === AST_NODE_TYPES.JSXMemberExpression) {
69
52
  current = current.object;
@@ -72,10 +55,10 @@ export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
72
55
  return current;
73
56
  }
74
57
  return null;
75
- }
58
+ };
76
59
 
77
60
  // Helper: Extract the component name from a JSXElement.
78
- function getJSXElementName(jsxElement: TSESTree.JSXElement | null) {
61
+ const getJSXElementName = (jsxElement: TSESTree.JSXElement | null) => {
79
62
  if (
80
63
  !jsxElement ||
81
64
  !jsxElement.openingElement ||
@@ -92,44 +75,35 @@ export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
92
75
  return rightmost.name;
93
76
  }
94
77
  return "";
95
- }
78
+ };
96
79
 
97
80
  // Helper: Check if the node is a call to useState.
98
- function isUseStateCall(
81
+ const isUseStateCall = (
99
82
  node: TSESTree.Node | null
100
- ): node is TSESTree.CallExpression {
101
- return (
102
- node !== null &&
103
- node.type === AST_NODE_TYPES.CallExpression &&
104
- node.callee !== null &&
105
- ((node.callee.type === AST_NODE_TYPES.Identifier &&
106
- node.callee.name === "useState") ||
107
- (node.callee.type === AST_NODE_TYPES.MemberExpression &&
108
- node.callee.property !== null &&
109
- node.callee.property.type ===
110
- AST_NODE_TYPES.Identifier &&
111
- node.callee.property.name === "useState"))
112
- );
113
- }
83
+ ): node is TSESTree.CallExpression =>
84
+ node !== null &&
85
+ node.type === AST_NODE_TYPES.CallExpression &&
86
+ node.callee !== null &&
87
+ ((node.callee.type === AST_NODE_TYPES.Identifier &&
88
+ node.callee.name === "useState") ||
89
+ (node.callee.type === AST_NODE_TYPES.MemberExpression &&
90
+ node.callee.property !== null &&
91
+ node.callee.property.type === AST_NODE_TYPES.Identifier &&
92
+ node.callee.property.name === "useState"));
114
93
 
115
94
  // Helper: Check if a call expression is a hook call (other than useState).
116
- function isHookCall(
95
+ const isHookCall = (
117
96
  node: TSESTree.Node | null
118
- ): node is TSESTree.CallExpression {
119
- return (
120
- node !== null &&
121
- node.type === AST_NODE_TYPES.CallExpression &&
122
- node.callee !== null &&
123
- node.callee.type === AST_NODE_TYPES.Identifier &&
124
- /^use[A-Z]/.test(node.callee.name) &&
125
- node.callee.name !== "useState"
126
- );
127
- }
97
+ ): node is TSESTree.CallExpression =>
98
+ node !== null &&
99
+ node.type === AST_NODE_TYPES.CallExpression &&
100
+ node.callee !== null &&
101
+ node.callee.type === AST_NODE_TYPES.Identifier &&
102
+ /^use[A-Z]/.test(node.callee.name) &&
103
+ node.callee.name !== "useState";
128
104
 
129
105
  // Helper: Walk upward to find the closest JSXElement ancestor.
130
- function getJSXAncestor(
131
- node: TSESTree.Node
132
- ): TSESTree.JSXElement | null {
106
+ const getJSXAncestor = (node: TSESTree.Node) => {
133
107
  let current: TSESTree.Node | null | undefined = node.parent;
134
108
  while (current) {
135
109
  if (current.type === AST_NODE_TYPES.JSXElement) {
@@ -138,54 +112,49 @@ export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
138
112
  current = current.parent;
139
113
  }
140
114
  return null;
141
- }
115
+ };
116
+
117
+ const getTagNameFromOpening = (
118
+ openingElement: TSESTree.JSXOpeningElement
119
+ ) => {
120
+ const nameNode = openingElement.name;
121
+ if (nameNode.type === AST_NODE_TYPES.JSXIdentifier) {
122
+ return nameNode.name;
123
+ }
124
+ const rightmost = getRightmostJSXIdentifier(nameNode);
125
+ return rightmost ? rightmost.name : null;
126
+ };
127
+
128
+ const isProviderOrContext = (tagName: string) =>
129
+ tagName.endsWith("Provider") || tagName.endsWith("Context");
130
+
131
+ const isValueAttributeOnProvider = (node: TSESTree.Node) =>
132
+ node.type === AST_NODE_TYPES.JSXAttribute &&
133
+ node.name &&
134
+ node.name.type === AST_NODE_TYPES.JSXIdentifier &&
135
+ node.name.name === "value" &&
136
+ node.parent &&
137
+ node.parent.type === AST_NODE_TYPES.JSXOpeningElement &&
138
+ (() => {
139
+ const tagName = getTagNameFromOpening(node.parent);
140
+ return tagName !== null && isProviderOrContext(tagName);
141
+ })();
142
142
 
143
143
  // Helper: Check whether the given node is inside a JSXAttribute "value"
144
144
  // that belongs to a context-like component (i.e. tag name ends with Provider or Context).
145
- function isContextProviderValueProp(node: TSESTree.Node): boolean {
145
+ const isContextProviderValueProp = (node: TSESTree.Node) => {
146
146
  let current: TSESTree.Node | null | undefined = node.parent;
147
147
  while (current) {
148
- if (
149
- current.type === AST_NODE_TYPES.JSXAttribute &&
150
- current.name &&
151
- current.name.type === AST_NODE_TYPES.JSXIdentifier &&
152
- current.name.name === "value"
153
- ) {
154
- // current.parent should be a JSXOpeningElement.
155
- if (
156
- current.parent &&
157
- current.parent.type === AST_NODE_TYPES.JSXOpeningElement
158
- ) {
159
- const nameNode = current.parent.name;
160
- if (nameNode.type === AST_NODE_TYPES.JSXIdentifier) {
161
- const tagName = nameNode.name;
162
- if (
163
- tagName.endsWith("Provider") ||
164
- tagName.endsWith("Context")
165
- ) {
166
- return true;
167
- }
168
- } else {
169
- const rightmost =
170
- getRightmostJSXIdentifier(nameNode);
171
- if (rightmost) {
172
- if (
173
- rightmost.name.endsWith("Provider") ||
174
- rightmost.name.endsWith("Context")
175
- ) {
176
- return true;
177
- }
178
- }
179
- }
180
- }
148
+ if (isValueAttributeOnProvider(current)) {
149
+ return true;
181
150
  }
182
151
  current = current.parent;
183
152
  }
184
153
  return false;
185
- }
154
+ };
186
155
 
187
156
  // Helper: Determine if a JSXElement is a custom component (tag name begins with an uppercase letter).
188
- function isCustomJSXElement(jsxElement: TSESTree.JSXElement | null) {
157
+ const isCustomJSXElement = (jsxElement: TSESTree.JSXElement | null) => {
189
158
  if (
190
159
  !jsxElement ||
191
160
  !jsxElement.openingElement ||
@@ -198,16 +167,11 @@ export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
198
167
  return /^[A-Z]/.test(nameNode.name);
199
168
  }
200
169
  const leftmost = getLeftmostJSXIdentifier(nameNode);
201
- if (leftmost && /^[A-Z]/.test(leftmost.name)) {
202
- return true;
203
- }
204
- return false;
205
- }
170
+ return leftmost !== null && /^[A-Z]/.test(leftmost.name);
171
+ };
206
172
 
207
173
  // Helper: Find the nearest enclosing function (assumed to be the component).
208
- function getComponentFunction(
209
- node: TSESTree.Node | null
210
- ): ComponentFunction | null {
174
+ const getComponentFunction = (node: TSESTree.Node | null) => {
211
175
  let current: TSESTree.Node | null | undefined = node;
212
176
  while (current) {
213
177
  if (
@@ -220,118 +184,232 @@ export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
220
184
  current = current.parent;
221
185
  }
222
186
  return null;
223
- }
187
+ };
224
188
 
225
- function findVariableForIdentifier(
226
- id: TSESTree.Identifier
227
- ): TSESLint.Scope.Variable | null {
189
+ const findVariableForIdentifier = (identifier: TSESTree.Identifier) => {
228
190
  let scope: TSESLint.Scope.Scope | null =
229
- context.sourceCode.getScope(id);
191
+ context.sourceCode.getScope(identifier);
230
192
  while (scope) {
231
- for (const variable of scope.variables) {
232
- for (const def of variable.defs) {
233
- if (def.name === id) {
234
- return variable;
235
- }
236
- }
193
+ const found = scope.variables.find((variable) =>
194
+ variable.defs.some((def) => def.name === identifier)
195
+ );
196
+ if (found) {
197
+ return found;
237
198
  }
238
199
  scope = scope.upper ?? null;
239
200
  }
240
201
  return null;
241
- }
202
+ };
242
203
 
243
204
  // Analyze variable usage using ESLint scopes (no manual AST crawling).
244
205
  // Only count a usage if it occurs inside a custom JSX element (and is not inside a context provider's "value" prop).
245
- function analyzeVariableUsage(
206
+ const classifyReference = (
207
+ reference: TSESLint.Scope.Reference,
208
+ declarationId: TSESTree.Identifier,
209
+ jsxUsageSet: Set<TSESTree.JSXElement>
210
+ ) => {
211
+ const { identifier } = reference;
212
+
213
+ if (
214
+ identifier === declarationId ||
215
+ isContextProviderValueProp(identifier)
216
+ ) {
217
+ return false;
218
+ }
219
+
220
+ const jsxAncestor = getJSXAncestor(identifier);
221
+ if (jsxAncestor && isCustomJSXElement(jsxAncestor)) {
222
+ jsxUsageSet.add(jsxAncestor);
223
+ return false;
224
+ }
225
+ return true;
226
+ };
227
+
228
+ const analyzeVariableUsage = (
246
229
  declarationId: TSESTree.Identifier
247
- ): Usage {
230
+ ): Usage => {
248
231
  const variable = findVariableForIdentifier(declarationId);
249
232
  if (!variable) {
250
233
  return {
251
- jsxUsageSet: new Set<TSESTree.JSXElement>(),
252
- hasOutsideUsage: false
234
+ hasOutsideUsage: false,
235
+ jsxUsageSet: new Set<TSESTree.JSXElement>()
253
236
  };
254
237
  }
255
238
 
256
239
  const jsxUsageSet = new Set<TSESTree.JSXElement>();
257
- let hasOutsideUsage = false;
258
-
259
- for (const reference of variable.references) {
260
- const identifier = reference.identifier;
261
-
262
- if (identifier === declarationId) {
263
- continue;
264
- }
265
-
266
- // If the identifier is inside a "value" prop on a context-like component, ignore it.
267
- if (isContextProviderValueProp(identifier)) {
268
- continue;
269
- }
270
-
271
- const jsxAncestor = getJSXAncestor(identifier);
272
- if (jsxAncestor && isCustomJSXElement(jsxAncestor)) {
273
- jsxUsageSet.add(jsxAncestor);
274
- } else {
275
- hasOutsideUsage = true;
276
- }
277
- }
240
+ const hasOutsideUsage = variable.references.some((ref) =>
241
+ classifyReference(ref, declarationId, jsxUsageSet)
242
+ );
278
243
 
279
244
  return {
280
- jsxUsageSet,
281
- hasOutsideUsage
245
+ hasOutsideUsage,
246
+ jsxUsageSet
282
247
  };
283
- }
248
+ };
284
249
 
285
250
  // Manage hook-derived variables.
286
251
  const componentHookVars = new WeakMap<ComponentFunction, Set<string>>();
287
- function getHookSet(componentFunction: ComponentFunction): Set<string> {
252
+ const getHookSet = (componentFunction: ComponentFunction) => {
288
253
  let hookSet = componentHookVars.get(componentFunction);
289
254
  if (!hookSet) {
290
255
  hookSet = new Set<string>();
291
256
  componentHookVars.set(componentFunction, hookSet);
292
257
  }
293
258
  return hookSet;
294
- }
259
+ };
295
260
 
296
- function hasHookDependency(
261
+ const isRangeContained = (
262
+ refRange: [number, number],
263
+ nodeRange: [number, number]
264
+ ) => refRange[0] >= nodeRange[0] && refRange[1] <= nodeRange[1];
265
+
266
+ const variableHasReferenceInRange = (
267
+ variable: TSESLint.Scope.Variable,
268
+ nodeRange: [number, number]
269
+ ) =>
270
+ variable.references.some(
271
+ (reference) =>
272
+ reference.identifier.range !== undefined &&
273
+ isRangeContained(reference.identifier.range, nodeRange)
274
+ );
275
+
276
+ const hasHookDependency = (
297
277
  node: TSESTree.Node,
298
278
  hookSet: Set<string>
299
- ): boolean {
279
+ ) => {
300
280
  if (!node.range) {
301
281
  return false;
302
282
  }
303
283
  const nodeRange = node.range;
304
- const nodeStart = nodeRange[0];
305
- const nodeEnd = nodeRange[1];
306
284
 
307
285
  let scope: TSESLint.Scope.Scope | null =
308
286
  context.sourceCode.getScope(node);
309
287
 
310
288
  while (scope) {
311
- for (const variable of scope.variables) {
312
- if (!hookSet.has(variable.name)) {
313
- continue;
314
- }
315
- for (const reference of variable.references) {
316
- const identifier = reference.identifier;
317
- if (!identifier.range) {
318
- continue;
319
- }
320
- const refRange = identifier.range;
321
- const refStart = refRange[0];
322
- const refEnd = refRange[1];
323
- if (refStart >= nodeStart && refEnd <= nodeEnd) {
324
- return true;
325
- }
326
- }
289
+ const hookVars = scope.variables.filter((variable) =>
290
+ hookSet.has(variable.name)
291
+ );
292
+ if (
293
+ hookVars.some((variable) =>
294
+ variableHasReferenceInRange(variable, nodeRange)
295
+ )
296
+ ) {
297
+ return true;
327
298
  }
328
299
  scope = scope.upper ?? null;
329
300
  }
330
301
 
331
302
  return false;
332
- }
303
+ };
304
+
305
+ const processUseStateDeclarator = (
306
+ node: TSESTree.VariableDeclarator
307
+ ) => {
308
+ if (
309
+ !node.init ||
310
+ !isUseStateCall(node.init) ||
311
+ node.id.type !== AST_NODE_TYPES.ArrayPattern ||
312
+ node.id.elements.length < 2
313
+ ) {
314
+ return false;
315
+ }
316
+
317
+ const [stateElem, setterElem] = node.id.elements;
318
+ if (
319
+ !stateElem ||
320
+ stateElem.type !== AST_NODE_TYPES.Identifier ||
321
+ !setterElem ||
322
+ setterElem.type !== AST_NODE_TYPES.Identifier
323
+ ) {
324
+ return false;
325
+ }
326
+
327
+ const stateVarName = stateElem.name;
328
+ const setterVarName = setterElem.name;
329
+
330
+ const stateUsage = analyzeVariableUsage(stateElem);
331
+ const setterUsage = analyzeVariableUsage(setterElem);
332
+
333
+ const stateExclusivelySingleJSX =
334
+ !stateUsage.hasOutsideUsage &&
335
+ stateUsage.jsxUsageSet.size === 1;
336
+ const setterExclusivelySingleJSX =
337
+ !setterUsage.hasOutsideUsage &&
338
+ setterUsage.jsxUsageSet.size === 1;
339
+
340
+ if (!stateExclusivelySingleJSX || !setterExclusivelySingleJSX) {
341
+ return true;
342
+ }
343
+
344
+ const stateTarget = getSingleSetElement(stateUsage.jsxUsageSet);
345
+ const setterTarget = getSingleSetElement(setterUsage.jsxUsageSet);
346
+ if (stateTarget && stateTarget === setterTarget) {
347
+ context.report({
348
+ data: { setterVarName, stateVarName },
349
+ messageId: "stateAndSetterToChild",
350
+ node: node
351
+ });
352
+ }
353
+ return true;
354
+ };
355
+
356
+ const processGeneralVariable = (
357
+ node: TSESTree.VariableDeclarator,
358
+ componentFunction: ComponentFunction
359
+ ) => {
360
+ if (!node.id || node.id.type !== AST_NODE_TYPES.Identifier) {
361
+ return;
362
+ }
363
+
364
+ const varName = node.id.name;
365
+ // Exempt variables that depend on hooks.
366
+ if (node.init) {
367
+ const hookSet = getHookSet(componentFunction);
368
+ if (hasHookDependency(node.init, hookSet)) {
369
+ return;
370
+ }
371
+ }
372
+ const usage = analyzeVariableUsage(node.id);
373
+ if (!usage.hasOutsideUsage && usage.jsxUsageSet.size === 1) {
374
+ const target = getSingleSetElement(usage.jsxUsageSet);
375
+ const componentName = getJSXElementName(target);
376
+ candidateVariables.push({
377
+ componentName,
378
+ node,
379
+ varName
380
+ });
381
+ }
382
+ };
333
383
 
334
384
  return {
385
+ // At the end of the traversal, group candidate variables by the target component name.
386
+ "Program:exit"() {
387
+ const groups = new Map<string, CandidateVariable[]>();
388
+ candidateVariables.forEach((candidate) => {
389
+ const key = candidate.componentName;
390
+ const existing = groups.get(key);
391
+ if (existing) {
392
+ existing.push(candidate);
393
+ } else {
394
+ groups.set(key, [candidate]);
395
+ }
396
+ });
397
+ // Only report candidates for a given component type if there is exactly one candidate.
398
+ groups.forEach((candidates) => {
399
+ if (candidates.length !== 1) {
400
+ return;
401
+ }
402
+ const [candidate] = candidates;
403
+ if (!candidate) {
404
+ return;
405
+ }
406
+ context.report({
407
+ data: { varName: candidate.varName },
408
+ messageId: "variableToChild",
409
+ node: candidate.node
410
+ });
411
+ });
412
+ },
335
413
  VariableDeclarator(node: TSESTree.VariableDeclarator) {
336
414
  const componentFunction = getComponentFunction(node);
337
415
  if (!componentFunction || !componentFunction.body) return;
@@ -349,111 +427,28 @@ export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
349
427
  }
350
428
 
351
429
  // Case 1: useState destructuring (state & setter).
352
- if (
353
- node.init &&
354
- isUseStateCall(node.init) &&
355
- node.id.type === AST_NODE_TYPES.ArrayPattern &&
356
- node.id.elements.length >= 2
357
- ) {
358
- const stateElem = node.id.elements[0];
359
- const setterElem = node.id.elements[1];
360
- if (
361
- !stateElem ||
362
- stateElem.type !== AST_NODE_TYPES.Identifier ||
363
- !setterElem ||
364
- setterElem.type !== AST_NODE_TYPES.Identifier
365
- ) {
366
- return;
367
- }
368
- const stateVarName = stateElem.name;
369
- const setterVarName = setterElem.name;
370
-
371
- const stateUsage = analyzeVariableUsage(stateElem);
372
- const setterUsage = analyzeVariableUsage(setterElem);
373
-
374
- const stateExclusivelySingleJSX =
375
- !stateUsage.hasOutsideUsage &&
376
- stateUsage.jsxUsageSet.size === 1;
377
- const setterExclusivelySingleJSX =
378
- !setterUsage.hasOutsideUsage &&
379
- setterUsage.jsxUsageSet.size === 1;
380
- // Report immediately if both the state and setter are used exclusively
381
- // in the same single custom JSX element.
382
- if (
383
- stateExclusivelySingleJSX &&
384
- setterExclusivelySingleJSX
385
- ) {
386
- const stateTarget = getSingleSetElement(
387
- stateUsage.jsxUsageSet
388
- );
389
- const setterTarget = getSingleSetElement(
390
- setterUsage.jsxUsageSet
391
- );
392
- if (stateTarget && stateTarget === setterTarget) {
393
- context.report({
394
- node: node,
395
- messageId: "stateAndSetterToChild",
396
- data: { stateVarName, setterVarName }
397
- });
398
- }
399
- }
400
- }
430
+ const wasUseState = processUseStateDeclarator(node);
431
+
401
432
  // Case 2: General variable.
402
- else if (
403
- node.id &&
404
- node.id.type === AST_NODE_TYPES.Identifier
405
- ) {
406
- const varName = node.id.name;
407
- // Exempt variables that depend on hooks.
408
- if (node.init) {
409
- const hookSet = getHookSet(componentFunction);
410
- if (hasHookDependency(node.init, hookSet)) {
411
- return;
412
- }
413
- }
414
- const usage = analyzeVariableUsage(node.id);
415
- // Instead of reporting immediately, add a candidate if the variable is used exclusively in a single custom JSX element.
416
- if (
417
- !usage.hasOutsideUsage &&
418
- usage.jsxUsageSet.size === 1
419
- ) {
420
- const target = getSingleSetElement(usage.jsxUsageSet);
421
- const componentName = getJSXElementName(target);
422
- candidateVariables.push({
423
- node,
424
- varName,
425
- componentName
426
- });
427
- }
428
- }
429
- },
430
- // At the end of the traversal, group candidate variables by the target component name.
431
- "Program:exit"() {
432
- const groups = new Map<string, CandidateVariable[]>();
433
- for (const candidate of candidateVariables) {
434
- const key = candidate.componentName;
435
- const existing = groups.get(key);
436
- if (existing) {
437
- existing.push(candidate);
438
- } else {
439
- groups.set(key, [candidate]);
440
- }
441
- }
442
- // Only report candidates for a given component type if there is exactly one candidate.
443
- for (const candidates of groups.values()) {
444
- if (candidates.length === 1) {
445
- const candidate = candidates[0];
446
- if (!candidate) {
447
- continue;
448
- }
449
- context.report({
450
- node: candidate.node,
451
- messageId: "variableToChild",
452
- data: { varName: candidate.varName }
453
- });
454
- }
433
+ if (!wasUseState) {
434
+ processGeneralVariable(node, componentFunction);
455
435
  }
456
436
  }
457
437
  };
438
+ },
439
+ defaultOptions: [],
440
+ meta: {
441
+ docs: {
442
+ description:
443
+ "Disallow variables that are only passed to a single custom child component. For useState, only report if both the state and its setter are exclusively passed to a single custom child. For general variables, only report if a given child receives exactly one such candidate – if two or more are passed to the same component type, they're assumed to be settings that belong on the parent."
444
+ },
445
+ messages: {
446
+ stateAndSetterToChild:
447
+ "State variable '{{stateVarName}}' and its setter '{{setterVarName}}' are only passed to a single custom child component. Consider moving the state into that component.",
448
+ variableToChild:
449
+ "Variable '{{varName}}' is only passed to a single custom child component. Consider moving it to that component."
450
+ },
451
+ schema: [],
452
+ type: "suggestion"
458
453
  }
459
454
  };