@eslint-react/core 3.0.0-next.7 → 3.0.0-next.71
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/dist/index.d.ts +342 -320
- package/dist/index.js +523 -516
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,12 +1,55 @@
|
|
|
1
|
-
import { findImportSource, findProperty, findVariable, getVariableDefinitionNode } from "@eslint-react/var";
|
|
2
1
|
import * as ast from "@eslint-react/ast";
|
|
3
|
-
import { constFalse,
|
|
2
|
+
import { constFalse, dual, flip, getOrElseUpdate, identity } from "@eslint-react/eff";
|
|
4
3
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
5
|
-
import {
|
|
6
|
-
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
4
|
+
import { findVariable, getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
7
5
|
import { P, match } from "ts-pattern";
|
|
6
|
+
import { IdGenerator, RE_ANNOTATION_JSX, RE_ANNOTATION_JSX_FRAG, RE_ANNOTATION_JSX_IMPORT_SOURCE, RE_ANNOTATION_JSX_RUNTIME, RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE } from "@eslint-react/shared";
|
|
7
|
+
import { resolve } from "@eslint-react/var";
|
|
8
8
|
import { AST_NODE_TYPES as AST_NODE_TYPES$1 } from "@typescript-eslint/utils";
|
|
9
9
|
|
|
10
|
+
//#region src/api/find-import-source.ts
|
|
11
|
+
/**
|
|
12
|
+
* Get the arguments of a require expression
|
|
13
|
+
* @param node The node to match
|
|
14
|
+
* @returns The require expression arguments or null if the node is not a require expression
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
function getRequireExpressionArguments(node) {
|
|
18
|
+
return match(node).with({
|
|
19
|
+
type: AST_NODE_TYPES.CallExpression,
|
|
20
|
+
arguments: P.select(),
|
|
21
|
+
callee: {
|
|
22
|
+
type: AST_NODE_TYPES.Identifier,
|
|
23
|
+
name: "require"
|
|
24
|
+
}
|
|
25
|
+
}, identity).with({
|
|
26
|
+
type: AST_NODE_TYPES.MemberExpression,
|
|
27
|
+
object: P.select()
|
|
28
|
+
}, getRequireExpressionArguments).otherwise(() => null);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Find the import source of a variable
|
|
32
|
+
* @param name The variable name
|
|
33
|
+
* @param initialScope The initial scope to search
|
|
34
|
+
* @returns The import source or null if not found
|
|
35
|
+
*/
|
|
36
|
+
function findImportSource(name, initialScope) {
|
|
37
|
+
const latestDef = findVariable(initialScope, name)?.defs.at(-1);
|
|
38
|
+
if (latestDef == null) return null;
|
|
39
|
+
const { node, parent } = latestDef;
|
|
40
|
+
if (node.type === AST_NODE_TYPES.VariableDeclarator && node.init != null) {
|
|
41
|
+
const { init } = node;
|
|
42
|
+
if (init.type === AST_NODE_TYPES.MemberExpression && init.object.type === AST_NODE_TYPES.Identifier) return findImportSource(init.object.name, initialScope);
|
|
43
|
+
if (init.type === AST_NODE_TYPES.Identifier) return findImportSource(init.name, initialScope);
|
|
44
|
+
const arg0 = getRequireExpressionArguments(init)?.[0];
|
|
45
|
+
if (arg0 == null || !ast.isLiteral(arg0, "string")) return null;
|
|
46
|
+
return arg0.value;
|
|
47
|
+
}
|
|
48
|
+
if (parent?.type === AST_NODE_TYPES.ImportDeclaration) return parent.source.value;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
10
53
|
//#region src/api/is-from-react.ts
|
|
11
54
|
/**
|
|
12
55
|
* Check if a variable is initialized from React import
|
|
@@ -260,7 +303,7 @@ function isHookId(id) {
|
|
|
260
303
|
|
|
261
304
|
//#endregion
|
|
262
305
|
//#region src/hook/hook-collector.ts
|
|
263
|
-
const idGen$2 = new IdGenerator("
|
|
306
|
+
const idGen$2 = new IdGenerator("hook:");
|
|
264
307
|
/**
|
|
265
308
|
* Get a ctx and visitor object for the rule to collect hooks
|
|
266
309
|
* @param context The ESLint rule context
|
|
@@ -270,7 +313,7 @@ function useHookCollector(context) {
|
|
|
270
313
|
const hooks = /* @__PURE__ */ new Map();
|
|
271
314
|
const functionEntries = [];
|
|
272
315
|
const getText = (n) => context.sourceCode.getText(n);
|
|
273
|
-
const getCurrentEntry = () => functionEntries.at(-1);
|
|
316
|
+
const getCurrentEntry = () => functionEntries.at(-1) ?? null;
|
|
274
317
|
const onFunctionEnter = (node) => {
|
|
275
318
|
const id = ast.getFunctionId(node);
|
|
276
319
|
const key = idGen$2.next();
|
|
@@ -284,11 +327,11 @@ function useHookCollector(context) {
|
|
|
284
327
|
key,
|
|
285
328
|
kind: "function",
|
|
286
329
|
name: ast.getFullyQualifiedName(id, getText),
|
|
287
|
-
node,
|
|
288
330
|
directives: [],
|
|
289
331
|
flag: 0n,
|
|
290
332
|
hint: 0n,
|
|
291
|
-
hookCalls: []
|
|
333
|
+
hookCalls: [],
|
|
334
|
+
node
|
|
292
335
|
});
|
|
293
336
|
};
|
|
294
337
|
const onFunctionExit = () => {
|
|
@@ -315,150 +358,6 @@ function useHookCollector(context) {
|
|
|
315
358
|
};
|
|
316
359
|
}
|
|
317
360
|
|
|
318
|
-
//#endregion
|
|
319
|
-
//#region src/jsx/jsx-stringify.ts
|
|
320
|
-
/**
|
|
321
|
-
* Incomplete but sufficient stringification of JSX nodes for common use cases
|
|
322
|
-
*
|
|
323
|
-
* @param node JSX node from TypeScript ESTree
|
|
324
|
-
* @returns String representation of the JSX node
|
|
325
|
-
*/
|
|
326
|
-
function stringifyJsx(node) {
|
|
327
|
-
switch (node.type) {
|
|
328
|
-
case AST_NODE_TYPES.JSXIdentifier: return node.name;
|
|
329
|
-
case AST_NODE_TYPES.JSXNamespacedName: return `${node.namespace.name}:${node.name.name}`;
|
|
330
|
-
case AST_NODE_TYPES.JSXMemberExpression: return `${stringifyJsx(node.object)}.${stringifyJsx(node.property)}`;
|
|
331
|
-
case AST_NODE_TYPES.JSXText: return node.value;
|
|
332
|
-
case AST_NODE_TYPES.JSXOpeningElement: return `<${stringifyJsx(node.name)}>`;
|
|
333
|
-
case AST_NODE_TYPES.JSXClosingElement: return `</${stringifyJsx(node.name)}>`;
|
|
334
|
-
case AST_NODE_TYPES.JSXOpeningFragment: return "<>";
|
|
335
|
-
case AST_NODE_TYPES.JSXClosingFragment: return "</>";
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
//#endregion
|
|
340
|
-
//#region src/jsx/jsx-attribute-name.ts
|
|
341
|
-
/**
|
|
342
|
-
* Get the stringified name of a JSX attribute
|
|
343
|
-
* @param context The ESLint rule context
|
|
344
|
-
* @param node The JSX attribute node
|
|
345
|
-
* @returns The name of the attribute
|
|
346
|
-
*/
|
|
347
|
-
function getJsxAttributeName(context, node) {
|
|
348
|
-
return stringifyJsx(node.name);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
//#endregion
|
|
352
|
-
//#region src/jsx/jsx-attribute.ts
|
|
353
|
-
/**
|
|
354
|
-
* Creates a helper function to find a specific JSX attribute by name
|
|
355
|
-
* Handles direct attributes and spread attributes (variables or object literals)
|
|
356
|
-
* @param context The ESLint rule context
|
|
357
|
-
* @param node The JSX element node
|
|
358
|
-
* @param initialScope (Optional) The initial scope to use for variable resolution
|
|
359
|
-
*/
|
|
360
|
-
function getJsxAttribute(context, node, initialScope) {
|
|
361
|
-
const scope = initialScope ?? context.sourceCode.getScope(node);
|
|
362
|
-
const attributes = node.openingElement.attributes;
|
|
363
|
-
/**
|
|
364
|
-
* Finds the last occurrence of a specific attribute
|
|
365
|
-
* @param name The attribute name to search for
|
|
366
|
-
*/
|
|
367
|
-
return (name) => {
|
|
368
|
-
return attributes.findLast((attr) => {
|
|
369
|
-
if (attr.type === AST_NODE_TYPES.JSXAttribute) return getJsxAttributeName(context, attr) === name;
|
|
370
|
-
switch (attr.argument.type) {
|
|
371
|
-
case AST_NODE_TYPES.Identifier: {
|
|
372
|
-
const variableNode = getVariableDefinitionNode(findVariable(attr.argument.name, scope), 0);
|
|
373
|
-
if (variableNode?.type === AST_NODE_TYPES.ObjectExpression) return findProperty(name, variableNode.properties, scope) != null;
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
376
|
-
case AST_NODE_TYPES.ObjectExpression: return findProperty(name, attr.argument.properties, scope) != null;
|
|
377
|
-
}
|
|
378
|
-
return false;
|
|
379
|
-
});
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
//#endregion
|
|
384
|
-
//#region src/jsx/jsx-attribute-value.ts
|
|
385
|
-
/**
|
|
386
|
-
* Resolve the static value of a JSX attribute or spread attribute
|
|
387
|
-
*
|
|
388
|
-
* @param context - The ESLint rule context
|
|
389
|
-
* @param attribute - The JSX attribute node to resolve
|
|
390
|
-
* @returns An object containing the value kind, the node (if applicable), and a `toStatic` helper
|
|
391
|
-
*/
|
|
392
|
-
function resolveJsxAttributeValue(context, attribute) {
|
|
393
|
-
const initialScope = context.sourceCode.getScope(attribute);
|
|
394
|
-
/**
|
|
395
|
-
* Handles standard JSX attributes (e.g., prop="value", prop={value}, prop)
|
|
396
|
-
* @param node The JSX attribute node
|
|
397
|
-
*/
|
|
398
|
-
function handleJsxAttribute(node) {
|
|
399
|
-
if (node.value == null) return {
|
|
400
|
-
kind: "boolean",
|
|
401
|
-
toStatic() {
|
|
402
|
-
return true;
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
switch (node.value.type) {
|
|
406
|
-
case AST_NODE_TYPES.Literal: {
|
|
407
|
-
const staticValue = node.value.value;
|
|
408
|
-
return {
|
|
409
|
-
kind: "literal",
|
|
410
|
-
node: node.value,
|
|
411
|
-
toStatic() {
|
|
412
|
-
return staticValue;
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
case AST_NODE_TYPES.JSXExpressionContainer: {
|
|
417
|
-
const expr = node.value.expression;
|
|
418
|
-
return {
|
|
419
|
-
kind: "expression",
|
|
420
|
-
node: expr,
|
|
421
|
-
toStatic() {
|
|
422
|
-
return getStaticValue(expr, initialScope)?.value;
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
case AST_NODE_TYPES.JSXElement: return {
|
|
427
|
-
kind: "element",
|
|
428
|
-
node: node.value,
|
|
429
|
-
toStatic() {
|
|
430
|
-
return unit;
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
case AST_NODE_TYPES.JSXSpreadChild: return {
|
|
434
|
-
kind: "spreadChild",
|
|
435
|
-
node: node.value.expression,
|
|
436
|
-
toStatic() {
|
|
437
|
-
return unit;
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Handles JSX spread attributes (e.g., {...props})
|
|
444
|
-
* @param node The JSX spread attribute node
|
|
445
|
-
*/
|
|
446
|
-
function handleJsxSpreadAttribute(node) {
|
|
447
|
-
return {
|
|
448
|
-
kind: "spreadProps",
|
|
449
|
-
node: node.argument,
|
|
450
|
-
toStatic(name) {
|
|
451
|
-
if (name == null) return unit;
|
|
452
|
-
return match(getStaticValue(node.argument, initialScope)?.value).with({ [name]: P.select(P.any) }, identity).otherwise(() => unit);
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
switch (attribute.type) {
|
|
457
|
-
case AST_NODE_TYPES.JSXAttribute: return handleJsxAttribute(attribute);
|
|
458
|
-
case AST_NODE_TYPES.JSXSpreadAttribute: return handleJsxSpreadAttribute(attribute);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
361
|
//#endregion
|
|
463
362
|
//#region src/jsx/jsx-config.ts
|
|
464
363
|
const JsxEmit = {
|
|
@@ -531,25 +430,15 @@ const JsxDetectionHint = {
|
|
|
531
430
|
*/
|
|
532
431
|
const DEFAULT_JSX_DETECTION_HINT = 0n | JsxDetectionHint.DoNotIncludeJsxWithNumberValue | JsxDetectionHint.DoNotIncludeJsxWithBigIntValue | JsxDetectionHint.DoNotIncludeJsxWithBooleanValue | JsxDetectionHint.DoNotIncludeJsxWithStringValue | JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue;
|
|
533
432
|
/**
|
|
534
|
-
* Check if a node is a `JSXText` or a `Literal` node
|
|
535
|
-
* @param node The AST node to check
|
|
536
|
-
* @returns `true` if the node is a `JSXText` or a `Literal` node
|
|
537
|
-
*/
|
|
538
|
-
function isJsxText(node) {
|
|
539
|
-
if (node == null) return false;
|
|
540
|
-
return node.type === AST_NODE_TYPES.JSXText || node.type === AST_NODE_TYPES.Literal;
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
433
|
* Determine if a node represents JSX-like content based on heuristics
|
|
544
434
|
* Supports configuration through hint flags to customize detection behavior
|
|
545
435
|
*
|
|
546
|
-
* @param
|
|
547
|
-
* @param code.getScope The function to get the scope of a node
|
|
436
|
+
* @param context The rule context with scope lookup capability
|
|
548
437
|
* @param node The AST node to analyze
|
|
549
438
|
* @param hint The configuration flags to adjust detection behavior
|
|
550
439
|
* @returns boolean Whether the node is considered JSX-like
|
|
551
440
|
*/
|
|
552
|
-
function isJsxLike(
|
|
441
|
+
function isJsxLike(context, node, hint = DEFAULT_JSX_DETECTION_HINT) {
|
|
553
442
|
if (node == null) return false;
|
|
554
443
|
if (ast.isJSX(node)) return true;
|
|
555
444
|
switch (node.type) {
|
|
@@ -565,27 +454,27 @@ function isJsxLike(code, node, hint = DEFAULT_JSX_DETECTION_HINT) {
|
|
|
565
454
|
case AST_NODE_TYPES.TemplateLiteral: return !(hint & JsxDetectionHint.DoNotIncludeJsxWithStringValue);
|
|
566
455
|
case AST_NODE_TYPES.ArrayExpression:
|
|
567
456
|
if (node.elements.length === 0) return !(hint & JsxDetectionHint.DoNotIncludeJsxWithEmptyArrayValue);
|
|
568
|
-
if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return node.elements.every((n) => isJsxLike(
|
|
569
|
-
return node.elements.some((n) => isJsxLike(
|
|
457
|
+
if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return node.elements.every((n) => isJsxLike(context, n, hint));
|
|
458
|
+
return node.elements.some((n) => isJsxLike(context, n, hint));
|
|
570
459
|
case AST_NODE_TYPES.LogicalExpression:
|
|
571
|
-
if (hint & JsxDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx) return isJsxLike(
|
|
572
|
-
return isJsxLike(
|
|
460
|
+
if (hint & JsxDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx) return isJsxLike(context, node.left, hint) && isJsxLike(context, node.right, hint);
|
|
461
|
+
return isJsxLike(context, node.left, hint) || isJsxLike(context, node.right, hint);
|
|
573
462
|
case AST_NODE_TYPES.ConditionalExpression: {
|
|
574
463
|
function leftHasJSX(node) {
|
|
575
464
|
if (Array.isArray(node.consequent)) {
|
|
576
465
|
if (node.consequent.length === 0) return !(hint & JsxDetectionHint.DoNotIncludeJsxWithEmptyArrayValue);
|
|
577
|
-
if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return node.consequent.every((n) => isJsxLike(
|
|
578
|
-
return node.consequent.some((n) => isJsxLike(
|
|
466
|
+
if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return node.consequent.every((n) => isJsxLike(context, n, hint));
|
|
467
|
+
return node.consequent.some((n) => isJsxLike(context, n, hint));
|
|
579
468
|
}
|
|
580
|
-
return isJsxLike(
|
|
469
|
+
return isJsxLike(context, node.consequent, hint);
|
|
581
470
|
}
|
|
582
471
|
function rightHasJSX(node) {
|
|
583
|
-
return isJsxLike(
|
|
472
|
+
return isJsxLike(context, node.alternate, hint);
|
|
584
473
|
}
|
|
585
474
|
if (hint & JsxDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx) return leftHasJSX(node) && rightHasJSX(node);
|
|
586
475
|
return leftHasJSX(node) || rightHasJSX(node);
|
|
587
476
|
}
|
|
588
|
-
case AST_NODE_TYPES.SequenceExpression: return isJsxLike(
|
|
477
|
+
case AST_NODE_TYPES.SequenceExpression: return isJsxLike(context, node.expressions.at(-1) ?? null, hint);
|
|
589
478
|
case AST_NODE_TYPES.CallExpression:
|
|
590
479
|
if (hint & JsxDetectionHint.DoNotIncludeJsxWithCreateElementValue) return false;
|
|
591
480
|
switch (node.callee.type) {
|
|
@@ -593,100 +482,321 @@ function isJsxLike(code, node, hint = DEFAULT_JSX_DETECTION_HINT) {
|
|
|
593
482
|
case AST_NODE_TYPES.MemberExpression: return node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "createElement";
|
|
594
483
|
}
|
|
595
484
|
return false;
|
|
596
|
-
case AST_NODE_TYPES.Identifier:
|
|
597
|
-
|
|
598
|
-
if (name === "undefined") return !(hint & JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue);
|
|
485
|
+
case AST_NODE_TYPES.Identifier:
|
|
486
|
+
if (node.name === "undefined") return !(hint & JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue);
|
|
599
487
|
if (ast.isJSXTagNameExpression(node)) return true;
|
|
600
|
-
return isJsxLike(
|
|
601
|
-
}
|
|
488
|
+
return isJsxLike(context, resolve(context, node), hint);
|
|
602
489
|
}
|
|
603
490
|
return false;
|
|
604
491
|
}
|
|
605
492
|
|
|
606
493
|
//#endregion
|
|
607
|
-
//#region src/jsx/jsx-
|
|
494
|
+
//#region src/jsx/jsx-stringify.ts
|
|
608
495
|
/**
|
|
609
|
-
*
|
|
610
|
-
* For JSX elements, returns the stringified name (e.g., "div", "Button", "React.Fragment")
|
|
611
|
-
* For JSX fragments, returns an empty string
|
|
496
|
+
* Incomplete but sufficient stringification of JSX nodes for common use cases
|
|
612
497
|
*
|
|
613
|
-
* @param
|
|
614
|
-
* @
|
|
615
|
-
* @returns String representation of the element type
|
|
498
|
+
* @param node JSX node from TypeScript ESTree
|
|
499
|
+
* @returns String representation of the JSX node
|
|
616
500
|
*/
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
|
|
501
|
+
function stringifyJsx(node) {
|
|
502
|
+
switch (node.type) {
|
|
503
|
+
case AST_NODE_TYPES.JSXIdentifier: return node.name;
|
|
504
|
+
case AST_NODE_TYPES.JSXNamespacedName: return `${node.namespace.name}:${node.name.name}`;
|
|
505
|
+
case AST_NODE_TYPES.JSXMemberExpression: return `${stringifyJsx(node.object)}.${stringifyJsx(node.property)}`;
|
|
506
|
+
case AST_NODE_TYPES.JSXText: return node.value;
|
|
507
|
+
case AST_NODE_TYPES.JSXOpeningElement: return `<${stringifyJsx(node.name)}>`;
|
|
508
|
+
case AST_NODE_TYPES.JSXClosingElement: return `</${stringifyJsx(node.name)}>`;
|
|
509
|
+
case AST_NODE_TYPES.JSXOpeningFragment: return "<>";
|
|
510
|
+
case AST_NODE_TYPES.JSXClosingFragment: return "</>";
|
|
511
|
+
}
|
|
620
512
|
}
|
|
621
513
|
|
|
622
514
|
//#endregion
|
|
623
|
-
//#region src/jsx/jsx-
|
|
515
|
+
//#region src/jsx/jsx-inspector.ts
|
|
624
516
|
/**
|
|
625
|
-
*
|
|
626
|
-
*
|
|
517
|
+
* A stateful helper that binds an ESLint `RuleContext` once and exposes
|
|
518
|
+
* ergonomic methods for the most common JSX inspection tasks that rules need.
|
|
627
519
|
*
|
|
628
|
-
*
|
|
629
|
-
* @param node AST node to check
|
|
630
|
-
* @returns boolean indicating if the element is a host element
|
|
631
|
-
*/
|
|
632
|
-
function isJsxHostElement(context, node) {
|
|
633
|
-
return node.type === AST_NODE_TYPES.JSXElement && node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && /^[a-z]/u.test(node.openingElement.name.name);
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Determine if a JSX element is a React Fragment
|
|
637
|
-
* Fragments can be imported from React and used like <Fragment> or <React.Fragment>
|
|
520
|
+
* ### Typical usage inside a rule's `create` function
|
|
638
521
|
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
* @param jsxConfig.jsxFragmentFactory Name of the fragment factory (e.g., React.Fragment)
|
|
643
|
-
* @returns boolean indicating if the element is a Fragment
|
|
644
|
-
*/
|
|
645
|
-
function isJsxFragmentElement(context, node, jsxConfig) {
|
|
646
|
-
if (node.type !== AST_NODE_TYPES.JSXElement) return false;
|
|
647
|
-
const fragment = jsxConfig?.jsxFragmentFactory?.split(".").at(-1) ?? "Fragment";
|
|
648
|
-
return getJsxElementType(context, node).split(".").at(-1) === fragment;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
//#endregion
|
|
652
|
-
//#region src/jsx/jsx-hierarchy.ts
|
|
653
|
-
/**
|
|
654
|
-
* Traverses up the AST to find a parent JSX attribute node that matches a given test
|
|
522
|
+
* ```ts
|
|
523
|
+
* export function create(context: RuleContext) {
|
|
524
|
+
* const jsx = JsxInspector.from(context);
|
|
655
525
|
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
526
|
+
* return defineRuleListener({
|
|
527
|
+
* JSXElement(node) {
|
|
528
|
+
* // element type
|
|
529
|
+
* const type = jsx.getElementType(node); // "div" | "React.Fragment" | …
|
|
530
|
+
*
|
|
531
|
+
* // attribute lookup + value resolution in one step
|
|
532
|
+
* const val = jsx.getAttributeValue(node, "sandbox");
|
|
533
|
+
* if (typeof val?.getStatic() === "string") { … }
|
|
534
|
+
*
|
|
535
|
+
* // simple boolean checks
|
|
536
|
+
* if (jsx.isHostElement(node)) { … }
|
|
537
|
+
* if (jsx.isFragmentElement(node)) { … }
|
|
538
|
+
* if (jsx.hasAttribute(node, "key")) { … }
|
|
539
|
+
* },
|
|
540
|
+
* });
|
|
541
|
+
* }
|
|
542
|
+
* ```
|
|
660
543
|
*/
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
544
|
+
var JsxInspector = class JsxInspector {
|
|
545
|
+
context;
|
|
546
|
+
/**
|
|
547
|
+
* Merged JSX configuration (tsconfig compiler options + pragma annotations).
|
|
548
|
+
* The result is lazily computed and cached for the lifetime of this inspector.
|
|
549
|
+
*/
|
|
550
|
+
get jsxConfig() {
|
|
551
|
+
return this.#jsxConfig ??= {
|
|
552
|
+
...getJsxConfigFromContext(this.context),
|
|
553
|
+
...getJsxConfigFromAnnotation(this.context)
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Lazily resolved & cached JSX configuration (merged from tsconfig +
|
|
558
|
+
* pragma annotations). Use {@link jsxConfig} to access.
|
|
559
|
+
*/
|
|
560
|
+
#jsxConfig;
|
|
561
|
+
constructor(context) {
|
|
562
|
+
this.context = context;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Walk **up** the AST from `node` to find the nearest ancestor that is a
|
|
566
|
+
* `JSXAttribute` and passes the optional `test` predicate.
|
|
567
|
+
* @param node The starting node for the search.
|
|
568
|
+
* @param test A predicate function to test each ancestor node.
|
|
569
|
+
*/
|
|
570
|
+
static findParentAttribute(node, test = () => true) {
|
|
571
|
+
const guard = (n) => {
|
|
572
|
+
return n.type === AST_NODE_TYPES.JSXAttribute && test(n);
|
|
573
|
+
};
|
|
574
|
+
return ast.findParentNode(node, guard);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Create a new `JsxInspector` bound to the given rule context.
|
|
578
|
+
* @param context The ESLint rule context to bind to this inspector instance.
|
|
579
|
+
*/
|
|
580
|
+
static from(context) {
|
|
581
|
+
return new JsxInspector(context);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Whether the node is a `JSXText` or a `Literal` node.
|
|
585
|
+
* @param node The node to check.
|
|
586
|
+
*/
|
|
587
|
+
static isJsxText(node) {
|
|
588
|
+
if (node == null) return false;
|
|
589
|
+
return node.type === AST_NODE_TYPES.JSXText || node.type === AST_NODE_TYPES.Literal;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Find a JSX attribute (or spread attribute containing the property) by name
|
|
593
|
+
* on a given element.
|
|
594
|
+
*
|
|
595
|
+
* Returns the **last** matching attribute (to mirror React's behaviour where
|
|
596
|
+
* later props win), or `undefined` if not found.
|
|
597
|
+
* @param node The JSX element to search for the attribute.
|
|
598
|
+
* @param name The name of the attribute to find (e.g. `"className"`).
|
|
599
|
+
*/
|
|
600
|
+
findAttribute(node, name) {
|
|
601
|
+
return node.openingElement.attributes.findLast((attr) => {
|
|
602
|
+
if (attr.type === AST_NODE_TYPES.JSXAttribute) return stringifyJsx(attr.name) === name;
|
|
603
|
+
switch (attr.argument.type) {
|
|
604
|
+
case AST_NODE_TYPES.Identifier: {
|
|
605
|
+
const initNode = resolve(this.context, attr.argument);
|
|
606
|
+
if (initNode?.type === AST_NODE_TYPES.ObjectExpression) return ast.findProperty(initNode.properties, name) != null;
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
case AST_NODE_TYPES.ObjectExpression: return ast.findProperty(attr.argument.properties, name) != null;
|
|
610
|
+
}
|
|
611
|
+
return false;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Get the stringified name of a `JSXAttribute` node
|
|
616
|
+
* (e.g. `"className"`, `"aria-label"`, `"xml:space"`).
|
|
617
|
+
* @param node The `JSXAttribute` node to extract the name from.
|
|
618
|
+
* @returns The stringified name of the attribute.
|
|
619
|
+
*/
|
|
620
|
+
getAttributeName(node) {
|
|
621
|
+
return stringifyJsx(node.name);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Resolve the static value of an attribute, automatically handling the
|
|
625
|
+
* `spreadProps` case by extracting the named property.
|
|
626
|
+
*
|
|
627
|
+
* This eliminates the repetitive pattern:
|
|
628
|
+
* ```ts
|
|
629
|
+
* const v = core.resolveJsxAttributeValue(ctx, attr);
|
|
630
|
+
* const s = v.kind === "spreadProps" ? v.getProperty(name) : v.toStatic();
|
|
631
|
+
* ```
|
|
632
|
+
*
|
|
633
|
+
* Returns `undefined` when the attribute is not present or its value
|
|
634
|
+
* cannot be statically determined.
|
|
635
|
+
* @param node The JSX element to search for the attribute.
|
|
636
|
+
* @param name The name of the attribute to resolve (e.g. `"className"`).
|
|
637
|
+
* @returns The static value of the attribute, or `undefined` if not found or not statically resolvable.
|
|
638
|
+
*/
|
|
639
|
+
getAttributeStaticValue(node, name) {
|
|
640
|
+
const attr = this.findAttribute(node, name);
|
|
641
|
+
if (attr == null) return void 0;
|
|
642
|
+
const resolved = this.resolveAttributeValue(attr);
|
|
643
|
+
if (resolved.kind === "spreadProps") return resolved.getProperty(name);
|
|
644
|
+
return resolved.toStatic();
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* **All-in-one helper** – find an attribute by name on an element *and*
|
|
648
|
+
* resolve its value in a single call.
|
|
649
|
+
*
|
|
650
|
+
* Returns `undefined` when the attribute is not present.
|
|
651
|
+
* @param node The JSX element to search for the attribute.
|
|
652
|
+
* @param name The name of the attribute to find and resolve (e.g. `"className"`).
|
|
653
|
+
* @returns A descriptor of the attribute's value that can be further inspected, or `undefined` if the attribute is not found.
|
|
654
|
+
*/
|
|
655
|
+
getAttributeValue(node, name) {
|
|
656
|
+
const attr = this.findAttribute(node, name);
|
|
657
|
+
if (attr == null) return void 0;
|
|
658
|
+
return this.resolveAttributeValue(attr);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Get the **self name** (last segment) of a JSX element type.
|
|
662
|
+
*
|
|
663
|
+
* - `<Foo.Bar.Baz>` → `"Baz"`
|
|
664
|
+
* - `<div>` → `"div"`
|
|
665
|
+
* - `<></>` → `""`
|
|
666
|
+
* @param node The JSX element or fragment to extract the self name from.
|
|
667
|
+
*/
|
|
668
|
+
getElementSelfName(node) {
|
|
669
|
+
return this.getElementType(node).split(".").at(-1) ?? "";
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Get the string representation of a JSX element's type.
|
|
673
|
+
*
|
|
674
|
+
* - `<div>` → `"div"`
|
|
675
|
+
* - `<Foo.Bar>` → `"Foo.Bar"`
|
|
676
|
+
* - `<React.Fragment>` → `"React.Fragment"`
|
|
677
|
+
* - `<></>` (JSXFragment) → `""`
|
|
678
|
+
* @param node The JSX element or fragment to extract the type from.
|
|
679
|
+
*/
|
|
680
|
+
getElementType(node) {
|
|
681
|
+
if (node.type === AST_NODE_TYPES.JSXFragment) return "";
|
|
682
|
+
return stringifyJsx(node.openingElement.name);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Shorthand: check whether an attribute exists on the element.
|
|
686
|
+
* @param node The JSX element to check for the attribute.
|
|
687
|
+
* @param name The name of the attribute to check for (e.g. `"className"`).
|
|
688
|
+
* @returns `true` if the attribute exists on the element, `false` otherwise.
|
|
689
|
+
*/
|
|
690
|
+
hasAttribute(node, name) {
|
|
691
|
+
return this.findAttribute(node, name) != null;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Whether the node is a React **Fragment** element (either `<Fragment>` /
|
|
695
|
+
* `<React.Fragment>` or the shorthand `<>` syntax).
|
|
696
|
+
*
|
|
697
|
+
* The check honours the configured `jsxFragmentFactory`.
|
|
698
|
+
* @param node The node to check.
|
|
699
|
+
*/
|
|
700
|
+
isFragmentElement(node) {
|
|
701
|
+
if (node.type === AST_NODE_TYPES.JSXFragment) return true;
|
|
702
|
+
if (node.type !== AST_NODE_TYPES.JSXElement) return false;
|
|
703
|
+
const fragment = this.jsxConfig.jsxFragmentFactory.split(".").at(-1) ?? "Fragment";
|
|
704
|
+
return this.getElementType(node).split(".").at(-1) === fragment;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Whether the node is a **host** (intrinsic / DOM) element – i.e. its tag
|
|
708
|
+
* name starts with a lowercase letter.
|
|
709
|
+
* @param node The node to check.
|
|
710
|
+
*/
|
|
711
|
+
isHostElement(node) {
|
|
712
|
+
return node.type === AST_NODE_TYPES.JSXElement && node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && /^[a-z]/u.test(node.openingElement.name.name);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Resolve the *value* of a JSX attribute (or spread attribute) into a
|
|
716
|
+
* descriptor that can be inspected further.
|
|
717
|
+
*
|
|
718
|
+
* See {@link JsxAttributeValue} for the full set of `kind` discriminants.
|
|
719
|
+
* @param attribute The attribute node to resolve the value of.
|
|
720
|
+
* @returns A descriptor of the attribute's value that can be further inspected.
|
|
721
|
+
*/
|
|
722
|
+
resolveAttributeValue(attribute) {
|
|
723
|
+
if (attribute.type === AST_NODE_TYPES.JSXAttribute) return this.#resolveJsxAttribute(attribute);
|
|
724
|
+
return this.#resolveJsxSpreadAttribute(attribute);
|
|
725
|
+
}
|
|
726
|
+
#resolveJsxAttribute(node) {
|
|
727
|
+
const scope = this.context.sourceCode.getScope(node);
|
|
728
|
+
if (node.value == null) return {
|
|
729
|
+
kind: "boolean",
|
|
730
|
+
toStatic() {
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
switch (node.value.type) {
|
|
735
|
+
case AST_NODE_TYPES.Literal: {
|
|
736
|
+
const staticValue = node.value.value;
|
|
737
|
+
return {
|
|
738
|
+
kind: "literal",
|
|
739
|
+
node: node.value,
|
|
740
|
+
toStatic() {
|
|
741
|
+
return staticValue;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
case AST_NODE_TYPES.JSXExpressionContainer: {
|
|
746
|
+
const expr = node.value.expression;
|
|
747
|
+
if (expr.type === AST_NODE_TYPES.JSXEmptyExpression) return {
|
|
748
|
+
kind: "missing",
|
|
749
|
+
node: expr,
|
|
750
|
+
toStatic() {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
return {
|
|
755
|
+
kind: "expression",
|
|
756
|
+
node: expr,
|
|
757
|
+
toStatic() {
|
|
758
|
+
return getStaticValue(expr, scope)?.value;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
case AST_NODE_TYPES.JSXElement: return {
|
|
763
|
+
kind: "element",
|
|
764
|
+
node: node.value,
|
|
765
|
+
toStatic() {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
case AST_NODE_TYPES.JSXSpreadChild:
|
|
770
|
+
node.value.expression;
|
|
771
|
+
return {
|
|
772
|
+
kind: "spreadChild",
|
|
773
|
+
node: node.value.expression,
|
|
774
|
+
toStatic() {
|
|
775
|
+
return null;
|
|
776
|
+
},
|
|
777
|
+
getChildren(_at) {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
#resolveJsxSpreadAttribute(node) {
|
|
784
|
+
const scope = this.context.sourceCode.getScope(node);
|
|
785
|
+
return {
|
|
786
|
+
kind: "spreadProps",
|
|
787
|
+
node: node.argument,
|
|
788
|
+
toStatic() {
|
|
789
|
+
return null;
|
|
790
|
+
},
|
|
791
|
+
getProperty(name) {
|
|
792
|
+
return match(getStaticValue(node.argument, scope)?.value).with({ [name]: P.select(P.any) }, identity).otherwise(() => null);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
};
|
|
667
797
|
|
|
668
798
|
//#endregion
|
|
669
|
-
//#region src/component/component-detection-
|
|
670
|
-
/**
|
|
671
|
-
* Hints for component collector
|
|
672
|
-
*/
|
|
673
|
-
const ComponentDetectionHint = {
|
|
674
|
-
...JsxDetectionHint,
|
|
675
|
-
DoNotIncludeFunctionDefinedOnObjectMethod: 1n << 64n,
|
|
676
|
-
DoNotIncludeFunctionDefinedOnClassMethod: 1n << 65n,
|
|
677
|
-
DoNotIncludeFunctionDefinedOnClassProperty: 1n << 66n,
|
|
678
|
-
DoNotIncludeFunctionDefinedInArrayPattern: 1n << 67n,
|
|
679
|
-
DoNotIncludeFunctionDefinedInArrayExpression: 1n << 68n,
|
|
680
|
-
DoNotIncludeFunctionDefinedAsArrayMapCallback: 1n << 69n,
|
|
681
|
-
DoNotIncludeFunctionDefinedAsArrayFlatMapCallback: 1n << 70n
|
|
682
|
-
};
|
|
683
|
-
/**
|
|
684
|
-
* Default component detection hint
|
|
685
|
-
*/
|
|
686
|
-
const DEFAULT_COMPONENT_DETECTION_HINT = 0n | ComponentDetectionHint.DoNotIncludeJsxWithBigIntValue | ComponentDetectionHint.DoNotIncludeJsxWithBooleanValue | ComponentDetectionHint.DoNotIncludeJsxWithNumberValue | ComponentDetectionHint.DoNotIncludeJsxWithStringValue | ComponentDetectionHint.DoNotIncludeJsxWithUndefinedValue | ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback | ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback | ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayExpression | ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayPattern | ComponentDetectionHint.RequireAllArrayElementsToBeJsx | ComponentDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx | ComponentDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx;
|
|
687
|
-
|
|
688
|
-
//#endregion
|
|
689
|
-
//#region src/component/component-is.ts
|
|
799
|
+
//#region src/component/component-detection-legacy.ts
|
|
690
800
|
/**
|
|
691
801
|
* Check if a node is a React class component
|
|
692
802
|
* @param node The AST node to check
|
|
@@ -717,6 +827,99 @@ function isPureComponent(node) {
|
|
|
717
827
|
}
|
|
718
828
|
return false;
|
|
719
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Create a lifecycle method checker function
|
|
832
|
+
* @param methodName The lifecycle method name
|
|
833
|
+
* @param isStatic Whether the method is static
|
|
834
|
+
*/
|
|
835
|
+
function createLifecycleChecker(methodName, isStatic = false) {
|
|
836
|
+
return (node) => ast.isMethodOrProperty(node) && node.static === isStatic && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === methodName;
|
|
837
|
+
}
|
|
838
|
+
const isRender = createLifecycleChecker("render");
|
|
839
|
+
const isComponentDidCatch = createLifecycleChecker("componentDidCatch");
|
|
840
|
+
const isComponentDidMount = createLifecycleChecker("componentDidMount");
|
|
841
|
+
const isComponentDidUpdate = createLifecycleChecker("componentDidUpdate");
|
|
842
|
+
const isComponentWillMount = createLifecycleChecker("componentWillMount");
|
|
843
|
+
const isComponentWillReceiveProps = createLifecycleChecker("componentWillReceiveProps");
|
|
844
|
+
const isComponentWillUnmount = createLifecycleChecker("componentWillUnmount");
|
|
845
|
+
const isComponentWillUpdate = createLifecycleChecker("componentWillUpdate");
|
|
846
|
+
const isGetChildContext = createLifecycleChecker("getChildContext");
|
|
847
|
+
const isGetInitialState = createLifecycleChecker("getInitialState");
|
|
848
|
+
const isGetSnapshotBeforeUpdate = createLifecycleChecker("getSnapshotBeforeUpdate");
|
|
849
|
+
const isShouldComponentUpdate = createLifecycleChecker("shouldComponentUpdate");
|
|
850
|
+
const isUnsafeComponentWillMount = createLifecycleChecker("UNSAFE_componentWillMount");
|
|
851
|
+
const isUnsafeComponentWillReceiveProps = createLifecycleChecker("UNSAFE_componentWillReceiveProps");
|
|
852
|
+
const isUnsafeComponentWillUpdate = createLifecycleChecker("UNSAFE_componentWillUpdate");
|
|
853
|
+
const isGetDefaultProps = createLifecycleChecker("getDefaultProps", true);
|
|
854
|
+
const isGetDerivedStateFromProps = createLifecycleChecker("getDerivedStateFromProps", true);
|
|
855
|
+
const isGetDerivedStateFromError = createLifecycleChecker("getDerivedStateFromError", true);
|
|
856
|
+
/**
|
|
857
|
+
* Check if the given node is a componentDidMount callback
|
|
858
|
+
* @param node The node to check
|
|
859
|
+
* @returns True if the node is a componentDidMount callback, false otherwise
|
|
860
|
+
*/
|
|
861
|
+
function isComponentDidMountCallback(node) {
|
|
862
|
+
return ast.isFunction(node) && isComponentDidMount(node.parent) && node.parent.value === node;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Check if the given node is a componentWillUnmount callback
|
|
866
|
+
* @param node The node to check
|
|
867
|
+
* @returns True if the node is a componentWillUnmount callback, false otherwise
|
|
868
|
+
*/
|
|
869
|
+
function isComponentWillUnmountCallback(node) {
|
|
870
|
+
return ast.isFunction(node) && isComponentWillUnmount(node.parent) && node.parent.value === node;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Check whether given node is a render method of a class component
|
|
874
|
+
* @example
|
|
875
|
+
* ```tsx
|
|
876
|
+
* class Component extends React.Component {
|
|
877
|
+
* renderHeader = () => <div />;
|
|
878
|
+
* renderFooter = () => <div />;
|
|
879
|
+
* }
|
|
880
|
+
* ```
|
|
881
|
+
* @param node The AST node to check
|
|
882
|
+
* @returns `true` if node is a render function, `false` if not
|
|
883
|
+
*/
|
|
884
|
+
function isRenderMethodLike(node) {
|
|
885
|
+
return ast.isMethodOrProperty(node) && node.key.type === AST_NODE_TYPES.Identifier && node.key.name.startsWith("render") && node.parent.parent.type === AST_NODE_TYPES.ClassDeclaration;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Check if the given node is a function within a render method of a class component
|
|
889
|
+
*
|
|
890
|
+
* @param node The AST node to check
|
|
891
|
+
* @returns `true` if the node is a render function inside a class component
|
|
892
|
+
*
|
|
893
|
+
* @example
|
|
894
|
+
* ```tsx
|
|
895
|
+
* class Component extends React.Component {
|
|
896
|
+
* renderHeader = () => <div />; // Returns true
|
|
897
|
+
* }
|
|
898
|
+
* ```
|
|
899
|
+
*/
|
|
900
|
+
function isRenderMethodCallback(node) {
|
|
901
|
+
const parent = node.parent;
|
|
902
|
+
const greatGrandparent = parent.parent?.parent;
|
|
903
|
+
return greatGrandparent != null && isRenderMethodLike(parent) && isClassComponent(greatGrandparent);
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Check whether the given node is a this.setState() call
|
|
907
|
+
* @param node The node to check
|
|
908
|
+
* @internal
|
|
909
|
+
*/
|
|
910
|
+
function isThisSetState(node) {
|
|
911
|
+
const { callee } = node;
|
|
912
|
+
return callee.type === AST_NODE_TYPES.MemberExpression && ast.isThisExpressionLoose(callee.object) && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "setState";
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Check whether the given node is an assignment to this.state
|
|
916
|
+
* @param node The node to check
|
|
917
|
+
* @internal
|
|
918
|
+
*/
|
|
919
|
+
function isAssignmentToThisState(node) {
|
|
920
|
+
const { left } = node;
|
|
921
|
+
return left.type === AST_NODE_TYPES.MemberExpression && ast.isThisExpressionLoose(left.object) && ast.getPropertyName(left.property) === "state";
|
|
922
|
+
}
|
|
720
923
|
|
|
721
924
|
//#endregion
|
|
722
925
|
//#region src/component/component-wrapper.ts
|
|
@@ -771,7 +974,7 @@ function isComponentWrapperCallbackLoose(context, node) {
|
|
|
771
974
|
* Get function component identifier from `const Component = memo(() => {});`
|
|
772
975
|
* @param context The rule context
|
|
773
976
|
* @param node The function node to analyze
|
|
774
|
-
* @returns The function identifier or `
|
|
977
|
+
* @returns The function identifier or `null` if not found
|
|
775
978
|
*/
|
|
776
979
|
function getFunctionComponentId(context, node) {
|
|
777
980
|
const functionId = ast.getFunctionId(node);
|
|
@@ -779,7 +982,7 @@ function getFunctionComponentId(context, node) {
|
|
|
779
982
|
const { parent } = node;
|
|
780
983
|
if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.id;
|
|
781
984
|
if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent.parent) && parent.parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.parent.id;
|
|
782
|
-
return
|
|
985
|
+
return null;
|
|
783
986
|
}
|
|
784
987
|
|
|
785
988
|
//#endregion
|
|
@@ -814,75 +1017,24 @@ function isFunctionWithLooseComponentName(context, fn, allowNone = false) {
|
|
|
814
1017
|
}
|
|
815
1018
|
|
|
816
1019
|
//#endregion
|
|
817
|
-
//#region src/component/component-
|
|
1020
|
+
//#region src/component/component-detection.ts
|
|
818
1021
|
/**
|
|
819
|
-
*
|
|
820
|
-
* @example
|
|
821
|
-
* ```tsx
|
|
822
|
-
* class Component extends React.Component {
|
|
823
|
-
* renderHeader = () => <div />;
|
|
824
|
-
* renderFooter = () => <div />;
|
|
825
|
-
* }
|
|
826
|
-
* ```
|
|
827
|
-
* @param node The AST node to check
|
|
828
|
-
* @returns `true` if node is a render function, `false` if not
|
|
829
|
-
*/
|
|
830
|
-
function isRenderMethodLike(node) {
|
|
831
|
-
return ast.isMethodOrProperty(node) && node.key.type === AST_NODE_TYPES.Identifier && node.key.name.startsWith("render") && node.parent.parent.type === AST_NODE_TYPES.ClassDeclaration;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
//#endregion
|
|
835
|
-
//#region src/component/component-definition.ts
|
|
836
|
-
/**
|
|
837
|
-
* Check if the given node is a function within a render method of a class component.
|
|
838
|
-
*
|
|
839
|
-
* @param node The AST node to check
|
|
840
|
-
* @returns `true` if the node is a render function inside a class component
|
|
841
|
-
*
|
|
842
|
-
* @example
|
|
843
|
-
* ```tsx
|
|
844
|
-
* class Component extends React.Component {
|
|
845
|
-
* renderHeader = () => <div />; // Returns true
|
|
846
|
-
* }
|
|
847
|
-
* ```
|
|
848
|
-
*/
|
|
849
|
-
function isRenderMethodCallback(node) {
|
|
850
|
-
const parent = node.parent;
|
|
851
|
-
const greatGrandparent = parent.parent?.parent;
|
|
852
|
-
return greatGrandparent != null && isRenderMethodLike(parent) && isClassComponent(greatGrandparent);
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Check if a function node should be excluded based on provided detection hints
|
|
856
|
-
*
|
|
857
|
-
* @param node The function node to check
|
|
858
|
-
* @param hint Component detection hints as bit flags
|
|
859
|
-
* @returns `true` if the function matches an exclusion hint
|
|
1022
|
+
* Hints for component collector
|
|
860
1023
|
*/
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
1024
|
+
const ComponentDetectionHint = {
|
|
1025
|
+
...JsxDetectionHint,
|
|
1026
|
+
DoNotIncludeFunctionDefinedAsArrayFlatMapCallback: 1n << 17n,
|
|
1027
|
+
DoNotIncludeFunctionDefinedAsArrayMapCallback: 1n << 16n,
|
|
1028
|
+
DoNotIncludeFunctionDefinedInArrayExpression: 1n << 15n,
|
|
1029
|
+
DoNotIncludeFunctionDefinedInArrayPattern: 1n << 14n,
|
|
1030
|
+
DoNotIncludeFunctionDefinedOnClassMethod: 1n << 12n,
|
|
1031
|
+
DoNotIncludeFunctionDefinedOnClassProperty: 1n << 13n,
|
|
1032
|
+
DoNotIncludeFunctionDefinedOnObjectMethod: 1n << 11n
|
|
1033
|
+
};
|
|
873
1034
|
/**
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
* @param context The rule context
|
|
877
|
-
* @param node The AST node to check
|
|
878
|
-
* @returns `true` if the node is passed as a child to `createElement`
|
|
1035
|
+
* Default component detection hint
|
|
879
1036
|
*/
|
|
880
|
-
|
|
881
|
-
const parent = node.parent;
|
|
882
|
-
if (parent?.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
883
|
-
if (!isCreateElementCall(context, parent)) return false;
|
|
884
|
-
return parent.arguments.slice(2).some((arg) => arg === node);
|
|
885
|
-
}
|
|
1037
|
+
const DEFAULT_COMPONENT_DETECTION_HINT = 0n | ComponentDetectionHint.DoNotIncludeJsxWithBigIntValue | ComponentDetectionHint.DoNotIncludeJsxWithBooleanValue | ComponentDetectionHint.DoNotIncludeJsxWithNumberValue | ComponentDetectionHint.DoNotIncludeJsxWithStringValue | ComponentDetectionHint.DoNotIncludeJsxWithUndefinedValue | ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback | ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback | ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayExpression | ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayPattern | ComponentDetectionHint.RequireAllArrayElementsToBeJsx | ComponentDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx | ComponentDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx;
|
|
886
1038
|
/**
|
|
887
1039
|
* Determine if a function node represents a valid React component definition
|
|
888
1040
|
*
|
|
@@ -893,8 +1045,33 @@ function isChildrenOfCreateElement(context, node) {
|
|
|
893
1045
|
*/
|
|
894
1046
|
function isComponentDefinition(context, node, hint) {
|
|
895
1047
|
if (!isFunctionWithLooseComponentName(context, node, true)) return false;
|
|
896
|
-
|
|
897
|
-
|
|
1048
|
+
switch (true) {
|
|
1049
|
+
case node.parent.type === AST_NODE_TYPES.CallExpression && isCreateElementCall(context, node.parent) && node.parent.arguments.slice(2).some((arg) => arg === node): return false;
|
|
1050
|
+
case isRenderMethodCallback(node): return false;
|
|
1051
|
+
}
|
|
1052
|
+
switch (true) {
|
|
1053
|
+
case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.Property && node.parent.parent.type === AST_NODE_TYPES.ObjectExpression:
|
|
1054
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnObjectMethod) return false;
|
|
1055
|
+
break;
|
|
1056
|
+
case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.MethodDefinition:
|
|
1057
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassMethod) return false;
|
|
1058
|
+
break;
|
|
1059
|
+
case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.Property:
|
|
1060
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassProperty) return false;
|
|
1061
|
+
break;
|
|
1062
|
+
case node.parent.type === AST_NODE_TYPES.ArrayPattern:
|
|
1063
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayPattern) return false;
|
|
1064
|
+
break;
|
|
1065
|
+
case node.parent.type === AST_NODE_TYPES.ArrayExpression:
|
|
1066
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayExpression) return false;
|
|
1067
|
+
break;
|
|
1068
|
+
case node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee.type === AST_NODE_TYPES.MemberExpression && node.parent.callee.property.type === AST_NODE_TYPES.Identifier && node.parent.callee.property.name === "map":
|
|
1069
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback) return false;
|
|
1070
|
+
break;
|
|
1071
|
+
case node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee.type === AST_NODE_TYPES.MemberExpression && node.parent.callee.property.type === AST_NODE_TYPES.Identifier && node.parent.callee.property.name === "flatMap":
|
|
1072
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback) return false;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
898
1075
|
const significantParent = ast.findParentNode(node, ast.isOneOf([
|
|
899
1076
|
AST_NODE_TYPES.JSXExpressionContainer,
|
|
900
1077
|
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
@@ -913,15 +1090,12 @@ function isComponentDefinition(context, node, hint) {
|
|
|
913
1090
|
* Component flag constants
|
|
914
1091
|
*/
|
|
915
1092
|
const ComponentFlag = {
|
|
916
|
-
None: 0n,
|
|
917
|
-
PureComponent: 1n << 0n,
|
|
918
1093
|
CreateElement: 1n << 1n,
|
|
1094
|
+
ForwardRef: 1n << 3n,
|
|
919
1095
|
Memo: 1n << 2n,
|
|
920
|
-
|
|
1096
|
+
None: 0n,
|
|
1097
|
+
PureComponent: 1n << 0n
|
|
921
1098
|
};
|
|
922
|
-
|
|
923
|
-
//#endregion
|
|
924
|
-
//#region src/component/component-init-path.ts
|
|
925
1099
|
/**
|
|
926
1100
|
* Get component flag from init path
|
|
927
1101
|
* @param initPath The init path of the function component
|
|
@@ -936,7 +1110,7 @@ function getComponentFlagFromInitPath(initPath) {
|
|
|
936
1110
|
|
|
937
1111
|
//#endregion
|
|
938
1112
|
//#region src/component/component-collector.ts
|
|
939
|
-
const idGen$1 = new IdGenerator("
|
|
1113
|
+
const idGen$1 = new IdGenerator("function-component:");
|
|
940
1114
|
/**
|
|
941
1115
|
* Get a ctx and visitor object for the rule to collect function components
|
|
942
1116
|
* @param context The ESLint rule context
|
|
@@ -948,14 +1122,14 @@ function useComponentCollector(context, options = {}) {
|
|
|
948
1122
|
const functionEntries = [];
|
|
949
1123
|
const components = /* @__PURE__ */ new Map();
|
|
950
1124
|
const getText = (n) => context.sourceCode.getText(n);
|
|
951
|
-
const getCurrentEntry = () => functionEntries.at(-1);
|
|
1125
|
+
const getCurrentEntry = () => functionEntries.at(-1) ?? null;
|
|
952
1126
|
const onFunctionEnter = (node) => {
|
|
953
1127
|
const key = idGen$1.next();
|
|
954
1128
|
const exp = ast.findParentNode(node, (n) => n.type === AST_NODE_TYPES.ExportDefaultDeclaration);
|
|
955
1129
|
const isExportDefault = exp != null;
|
|
956
1130
|
const isExportDefaultDeclaration = exp != null && ast.getUnderlyingExpression(exp.declaration) === node;
|
|
957
1131
|
const id = getFunctionComponentId(context, node);
|
|
958
|
-
const name = id == null ?
|
|
1132
|
+
const name = id == null ? null : ast.getFullyQualifiedName(id, getText);
|
|
959
1133
|
const initPath = ast.getFunctionInitPath(node);
|
|
960
1134
|
const directives = ast.getFunctionDirectives(node);
|
|
961
1135
|
const entry = {
|
|
@@ -963,9 +1137,8 @@ function useComponentCollector(context, options = {}) {
|
|
|
963
1137
|
key,
|
|
964
1138
|
kind: "function-component",
|
|
965
1139
|
name,
|
|
966
|
-
node,
|
|
967
1140
|
directives,
|
|
968
|
-
displayName:
|
|
1141
|
+
displayName: null,
|
|
969
1142
|
flag: getComponentFlagFromInitPath(initPath),
|
|
970
1143
|
hint,
|
|
971
1144
|
hookCalls: [],
|
|
@@ -973,6 +1146,7 @@ function useComponentCollector(context, options = {}) {
|
|
|
973
1146
|
isComponentDefinition: isComponentDefinition(context, node, hint),
|
|
974
1147
|
isExportDefault,
|
|
975
1148
|
isExportDefaultDeclaration,
|
|
1149
|
+
node,
|
|
976
1150
|
rets: []
|
|
977
1151
|
};
|
|
978
1152
|
functionEntries.push(entry);
|
|
@@ -1002,13 +1176,13 @@ function useComponentCollector(context, options = {}) {
|
|
|
1002
1176
|
if (body.type === AST_NODE_TYPES.BlockStatement) return;
|
|
1003
1177
|
entry.rets.push(body);
|
|
1004
1178
|
if (!entry.isComponentDefinition) return;
|
|
1005
|
-
if (!components.has(entry.key) && !isJsxLike(context
|
|
1179
|
+
if (!components.has(entry.key) && !isJsxLike(context, body, hint)) return;
|
|
1006
1180
|
components.set(entry.key, entry);
|
|
1007
1181
|
},
|
|
1008
1182
|
...collectDisplayName ? { [ast.SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION](node) {
|
|
1009
1183
|
const { left, right } = node;
|
|
1010
1184
|
if (left.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1011
|
-
const componentName = left.object.type === AST_NODE_TYPES.Identifier ? left.object.name :
|
|
1185
|
+
const componentName = left.object.type === AST_NODE_TYPES.Identifier ? left.object.name : null;
|
|
1012
1186
|
const component = [...components.values()].findLast(({ name }) => name != null && name === componentName);
|
|
1013
1187
|
if (component == null) return;
|
|
1014
1188
|
component.displayName = right;
|
|
@@ -1027,7 +1201,7 @@ function useComponentCollector(context, options = {}) {
|
|
|
1027
1201
|
entry.rets.push(node.argument);
|
|
1028
1202
|
if (!entry.isComponentDefinition) return;
|
|
1029
1203
|
const { argument } = node;
|
|
1030
|
-
if (!components.has(entry.key) && !isJsxLike(context
|
|
1204
|
+
if (!components.has(entry.key) && !isJsxLike(context, argument, hint)) return;
|
|
1031
1205
|
components.set(entry.key, entry);
|
|
1032
1206
|
}
|
|
1033
1207
|
}
|
|
@@ -1036,7 +1210,7 @@ function useComponentCollector(context, options = {}) {
|
|
|
1036
1210
|
|
|
1037
1211
|
//#endregion
|
|
1038
1212
|
//#region src/component/component-collector-legacy.ts
|
|
1039
|
-
const idGen = new IdGenerator("
|
|
1213
|
+
const idGen = new IdGenerator("class-component:");
|
|
1040
1214
|
/**
|
|
1041
1215
|
* Get a ctx and visitor object for the rule to collect class componentss
|
|
1042
1216
|
* @param context The ESLint rule context
|
|
@@ -1052,18 +1226,18 @@ function useComponentCollectorLegacy(context) {
|
|
|
1052
1226
|
if (!isClassComponent(node)) return;
|
|
1053
1227
|
const id = ast.getClassId(node);
|
|
1054
1228
|
const key = idGen.next();
|
|
1055
|
-
const name = id == null ?
|
|
1229
|
+
const name = id == null ? null : ast.getFullyQualifiedName(id, getText);
|
|
1056
1230
|
const flag = isPureComponent(node) ? ComponentFlag.PureComponent : ComponentFlag.None;
|
|
1057
1231
|
components.set(key, {
|
|
1058
1232
|
id,
|
|
1059
1233
|
key,
|
|
1060
1234
|
kind: "class-component",
|
|
1061
1235
|
name,
|
|
1062
|
-
|
|
1063
|
-
displayName: unit,
|
|
1236
|
+
displayName: null,
|
|
1064
1237
|
flag,
|
|
1065
1238
|
hint: 0n,
|
|
1066
|
-
methods: []
|
|
1239
|
+
methods: [],
|
|
1240
|
+
node
|
|
1067
1241
|
});
|
|
1068
1242
|
};
|
|
1069
1243
|
return {
|
|
@@ -1074,179 +1248,6 @@ function useComponentCollectorLegacy(context) {
|
|
|
1074
1248
|
}
|
|
1075
1249
|
};
|
|
1076
1250
|
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Check whether the given node is a this.setState() call
|
|
1079
|
-
* @param node The node to check
|
|
1080
|
-
* @internal
|
|
1081
|
-
*/
|
|
1082
|
-
function isThisSetState(node) {
|
|
1083
|
-
const { callee } = node;
|
|
1084
|
-
return callee.type === AST_NODE_TYPES$1.MemberExpression && ast.isThisExpressionLoose(callee.object) && callee.property.type === AST_NODE_TYPES$1.Identifier && callee.property.name === "setState";
|
|
1085
|
-
}
|
|
1086
|
-
/**
|
|
1087
|
-
* Check whether the given node is an assignment to this.state
|
|
1088
|
-
* @param node The node to check
|
|
1089
|
-
* @internal
|
|
1090
|
-
*/
|
|
1091
|
-
function isAssignmentToThisState(node) {
|
|
1092
|
-
const { left } = node;
|
|
1093
|
-
return left.type === AST_NODE_TYPES$1.MemberExpression && ast.isThisExpressionLoose(left.object) && ast.getPropertyName(left.property) === "state";
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
//#endregion
|
|
1097
|
-
//#region src/component/component-method-is.ts
|
|
1098
|
-
/**
|
|
1099
|
-
* Create a lifecycle method checker function
|
|
1100
|
-
* @param methodName The lifecycle method name
|
|
1101
|
-
* @param isStatic Whether the method is static
|
|
1102
|
-
*/
|
|
1103
|
-
function createLifecycleChecker(methodName, isStatic = false) {
|
|
1104
|
-
return (node) => ast.isMethodOrProperty(node) && node.static === isStatic && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === methodName;
|
|
1105
|
-
}
|
|
1106
|
-
const isRender = createLifecycleChecker("render");
|
|
1107
|
-
const isComponentDidCatch = createLifecycleChecker("componentDidCatch");
|
|
1108
|
-
const isComponentDidMount = createLifecycleChecker("componentDidMount");
|
|
1109
|
-
const isComponentDidUpdate = createLifecycleChecker("componentDidUpdate");
|
|
1110
|
-
const isComponentWillMount = createLifecycleChecker("componentWillMount");
|
|
1111
|
-
const isComponentWillReceiveProps = createLifecycleChecker("componentWillReceiveProps");
|
|
1112
|
-
const isComponentWillUnmount = createLifecycleChecker("componentWillUnmount");
|
|
1113
|
-
const isComponentWillUpdate = createLifecycleChecker("componentWillUpdate");
|
|
1114
|
-
const isGetChildContext = createLifecycleChecker("getChildContext");
|
|
1115
|
-
const isGetInitialState = createLifecycleChecker("getInitialState");
|
|
1116
|
-
const isGetSnapshotBeforeUpdate = createLifecycleChecker("getSnapshotBeforeUpdate");
|
|
1117
|
-
const isShouldComponentUpdate = createLifecycleChecker("shouldComponentUpdate");
|
|
1118
|
-
const isUnsafeComponentWillMount = createLifecycleChecker("UNSAFE_componentWillMount");
|
|
1119
|
-
const isUnsafeComponentWillReceiveProps = createLifecycleChecker("UNSAFE_componentWillReceiveProps");
|
|
1120
|
-
const isUnsafeComponentWillUpdate = createLifecycleChecker("UNSAFE_componentWillUpdate");
|
|
1121
|
-
const isGetDefaultProps = createLifecycleChecker("getDefaultProps", true);
|
|
1122
|
-
const isGetDerivedStateFromProps = createLifecycleChecker("getDerivedStateFromProps", true);
|
|
1123
|
-
const isGetDerivedStateFromError = createLifecycleChecker("getDerivedStateFromError", true);
|
|
1124
|
-
|
|
1125
|
-
//#endregion
|
|
1126
|
-
//#region src/component/component-method-callback.ts
|
|
1127
|
-
/**
|
|
1128
|
-
* Check if the given node is a componentDidMount callback
|
|
1129
|
-
* @param node The node to check
|
|
1130
|
-
* @returns True if the node is a componentDidMount callback, false otherwise
|
|
1131
|
-
*/
|
|
1132
|
-
function isComponentDidMountCallback(node) {
|
|
1133
|
-
return ast.isFunction(node) && isComponentDidMount(node.parent) && node.parent.value === node;
|
|
1134
|
-
}
|
|
1135
|
-
/**
|
|
1136
|
-
* Check if the given node is a componentWillUnmount callback
|
|
1137
|
-
* @param node The node to check
|
|
1138
|
-
* @returns True if the node is a componentWillUnmount callback, false otherwise
|
|
1139
|
-
*/
|
|
1140
|
-
function isComponentWillUnmountCallback(node) {
|
|
1141
|
-
return ast.isFunction(node) && isComponentWillUnmount(node.parent) && node.parent.value === node;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
//#endregion
|
|
1145
|
-
//#region src/component/component-render-prop.ts
|
|
1146
|
-
/**
|
|
1147
|
-
* Unsafe check whether given node is a render function
|
|
1148
|
-
* ```tsx
|
|
1149
|
-
* const renderRow = () => <div />
|
|
1150
|
-
* ` ^^^^^^^^^^^^`
|
|
1151
|
-
* _ = <Component renderRow={() => <div />} />
|
|
1152
|
-
* ` ^^^^^^^^^^^^^ `
|
|
1153
|
-
* ```
|
|
1154
|
-
* @param context The rule context
|
|
1155
|
-
* @param node The AST node to check
|
|
1156
|
-
* @returns `true` if node is a render function, `false` if not
|
|
1157
|
-
*/
|
|
1158
|
-
function isRenderFunctionLoose(context, node) {
|
|
1159
|
-
if (!ast.isFunction(node)) return false;
|
|
1160
|
-
const id = ast.getFunctionId(node);
|
|
1161
|
-
switch (true) {
|
|
1162
|
-
case id?.type === AST_NODE_TYPES.Identifier: return id.name.startsWith("render");
|
|
1163
|
-
case id?.type === AST_NODE_TYPES.MemberExpression && id.property.type === AST_NODE_TYPES.Identifier: return id.property.name.startsWith("render");
|
|
1164
|
-
case node.parent.type === AST_NODE_TYPES.JSXExpressionContainer && node.parent.parent.type === AST_NODE_TYPES.JSXAttribute && node.parent.parent.name.type === AST_NODE_TYPES.JSXIdentifier: return node.parent.parent.name.name.startsWith("render");
|
|
1165
|
-
}
|
|
1166
|
-
return false;
|
|
1167
|
-
}
|
|
1168
|
-
/**
|
|
1169
|
-
* Unsafe check whether given JSXAttribute is a render prop
|
|
1170
|
-
* ```tsx
|
|
1171
|
-
* _ = <Component renderRow={() => <div />} />
|
|
1172
|
-
* ` ^^^^^^^^^^^^^^^^^^^^^^^^^ `
|
|
1173
|
-
* ```
|
|
1174
|
-
* @param context The rule context
|
|
1175
|
-
* @param node The AST node to check
|
|
1176
|
-
* @returns `true` if node is a render prop, `false` if not
|
|
1177
|
-
*/
|
|
1178
|
-
function isRenderPropLoose(context, node) {
|
|
1179
|
-
if (node.name.type !== AST_NODE_TYPES.JSXIdentifier) return false;
|
|
1180
|
-
return node.name.name.startsWith("render") && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && isRenderFunctionLoose(context, node.value.expression);
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* Unsafe check whether given node is declared directly inside a render property
|
|
1184
|
-
* ```tsx
|
|
1185
|
-
* const rows = { render: () => <div /> }
|
|
1186
|
-
* ` ^^^^^^^^^^^^^ `
|
|
1187
|
-
* _ = <Component rows={ [{ render: () => <div /> }] } />
|
|
1188
|
-
* ` ^^^^^^^^^^^^^ `
|
|
1189
|
-
* ```
|
|
1190
|
-
* @internal
|
|
1191
|
-
* @param node The AST node to check
|
|
1192
|
-
* @returns `true` if component is declared inside a render property, `false` if not
|
|
1193
|
-
*/
|
|
1194
|
-
function isDirectValueOfRenderPropertyLoose(node) {
|
|
1195
|
-
const matching = (node) => {
|
|
1196
|
-
return node.type === AST_NODE_TYPES.Property && node.key.type === AST_NODE_TYPES.Identifier && node.key.name.startsWith("render");
|
|
1197
|
-
};
|
|
1198
|
-
return matching(node) || node.parent != null && matching(node.parent);
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* Unsafe check whether given node is declared inside a render prop
|
|
1202
|
-
* ```tsx
|
|
1203
|
-
* _ = <Component renderRow={"node"} />
|
|
1204
|
-
* ` ^^^^^^ `
|
|
1205
|
-
* _ = <Component rows={ [{ render: "node" }] } />
|
|
1206
|
-
* ` ^^^^^^ `
|
|
1207
|
-
* ```
|
|
1208
|
-
* @param node The AST node to check
|
|
1209
|
-
* @returns `true` if component is declared inside a render prop, `false` if not
|
|
1210
|
-
*/
|
|
1211
|
-
function isDeclaredInRenderPropLoose(node) {
|
|
1212
|
-
if (isDirectValueOfRenderPropertyLoose(node)) return true;
|
|
1213
|
-
const parent = ast.findParentNode(node, ast.is(AST_NODE_TYPES.JSXExpressionContainer))?.parent;
|
|
1214
|
-
if (parent?.type !== AST_NODE_TYPES.JSXAttribute) return false;
|
|
1215
|
-
return parent.name.type === AST_NODE_TYPES.JSXIdentifier && parent.name.name.startsWith("render");
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
//#endregion
|
|
1219
|
-
//#region src/hierarchy/find-enclosing-component-or-hook.ts
|
|
1220
|
-
/**
|
|
1221
|
-
* Find the enclosing React component or hook for a given AST node
|
|
1222
|
-
* @param node The AST node to start the search from
|
|
1223
|
-
* @param test Optional test function to customize component or hook identification
|
|
1224
|
-
* @returns The enclosing component or hook node, or `null` if none is ASAST.
|
|
1225
|
-
*/
|
|
1226
|
-
function findEnclosingComponentOrHook(node, test = (n, name) => {
|
|
1227
|
-
if (name == null) return false;
|
|
1228
|
-
return isComponentNameLoose(name) || isHookName(name);
|
|
1229
|
-
}) {
|
|
1230
|
-
const enclosingNode = ast.findParentNode(node, (n) => {
|
|
1231
|
-
if (!ast.isFunction(n)) return false;
|
|
1232
|
-
return test(n, match(ast.getFunctionId(n)).with({ type: AST_NODE_TYPES.Identifier }, (id) => id.name).with({
|
|
1233
|
-
type: AST_NODE_TYPES.MemberExpression,
|
|
1234
|
-
property: { type: AST_NODE_TYPES.Identifier }
|
|
1235
|
-
}, (me) => me.property.name).otherwise(() => null));
|
|
1236
|
-
});
|
|
1237
|
-
return ast.isFunction(enclosingNode) ? enclosingNode : unit;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
//#endregion
|
|
1241
|
-
//#region src/hierarchy/is-inside-component-or-hook.ts
|
|
1242
|
-
/**
|
|
1243
|
-
* Check if a given AST node is inside a React component or hook
|
|
1244
|
-
* @param node The AST node to check
|
|
1245
|
-
* @returns True if the node is inside a component or hook, false otherwise
|
|
1246
|
-
*/
|
|
1247
|
-
function isInsideComponentOrHook(node) {
|
|
1248
|
-
return findEnclosingComponentOrHook(node) != null;
|
|
1249
|
-
}
|
|
1250
1251
|
|
|
1251
1252
|
//#endregion
|
|
1252
1253
|
//#region src/ref/ref-name.ts
|
|
@@ -1255,12 +1256,18 @@ function isInsideComponentOrHook(node) {
|
|
|
1255
1256
|
* @param name The name to check
|
|
1256
1257
|
* @returns True if the name is "ref" or ends with "Ref"
|
|
1257
1258
|
*/
|
|
1258
|
-
function
|
|
1259
|
+
function isRefLikeName(name) {
|
|
1259
1260
|
return name === "ref" || name.endsWith("Ref");
|
|
1260
1261
|
}
|
|
1261
1262
|
|
|
1262
1263
|
//#endregion
|
|
1263
|
-
//#region src/ref/
|
|
1264
|
+
//#region src/ref/ref-id.ts
|
|
1265
|
+
function isRefId(node) {
|
|
1266
|
+
return node.type === AST_NODE_TYPES.Identifier && isRefLikeName(node.name);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
//#endregion
|
|
1270
|
+
//#region src/ref/ref-init.ts
|
|
1264
1271
|
/**
|
|
1265
1272
|
* Check if the variable with the given name is initialized or derived from a ref
|
|
1266
1273
|
* @param name The variable name
|
|
@@ -1274,20 +1281,20 @@ function isInitializedFromRef(name, initialScope) {
|
|
|
1274
1281
|
* Get the init expression of a ref variable
|
|
1275
1282
|
* @param name The variable name
|
|
1276
1283
|
* @param initialScope The initial scope
|
|
1277
|
-
* @returns The init expression node if the variable is derived from a ref, or
|
|
1284
|
+
* @returns The init expression node if the variable is derived from a ref, or null otherwise
|
|
1278
1285
|
*/
|
|
1279
1286
|
function getRefInit(name, initialScope) {
|
|
1280
|
-
for (const { node } of findVariable(initialScope
|
|
1287
|
+
for (const { node } of findVariable(initialScope, name)?.defs ?? []) {
|
|
1281
1288
|
if (node.type !== AST_NODE_TYPES$1.VariableDeclarator) continue;
|
|
1282
1289
|
const init = node.init;
|
|
1283
1290
|
if (init == null) continue;
|
|
1284
1291
|
switch (true) {
|
|
1285
|
-
case init.type === AST_NODE_TYPES$1.MemberExpression && init.object.type === AST_NODE_TYPES$1.Identifier &&
|
|
1292
|
+
case init.type === AST_NODE_TYPES$1.MemberExpression && init.object.type === AST_NODE_TYPES$1.Identifier && isRefLikeName(init.object.name): return init;
|
|
1286
1293
|
case init.type === AST_NODE_TYPES$1.CallExpression && isUseRefCall(init): return init;
|
|
1287
1294
|
}
|
|
1288
1295
|
}
|
|
1289
|
-
return
|
|
1296
|
+
return null;
|
|
1290
1297
|
}
|
|
1291
1298
|
|
|
1292
1299
|
//#endregion
|
|
1293
|
-
export { ComponentDetectionHint, ComponentFlag, DEFAULT_COMPONENT_DETECTION_HINT, DEFAULT_JSX_DETECTION_HINT, JsxDetectionHint, JsxEmit,
|
|
1300
|
+
export { ComponentDetectionHint, ComponentFlag, DEFAULT_COMPONENT_DETECTION_HINT, DEFAULT_JSX_DETECTION_HINT, JsxDetectionHint, JsxEmit, JsxInspector, REACT_BUILTIN_HOOK_NAMES, findImportSource, getComponentFlagFromInitPath, getFunctionComponentId, getJsxConfigFromAnnotation, getJsxConfigFromContext, getRefInit, isAssignmentToThisState, isCaptureOwnerStack, isCaptureOwnerStackCall, isChildrenCount, isChildrenCountCall, isChildrenForEach, isChildrenForEachCall, isChildrenMap, isChildrenMapCall, isChildrenOnly, isChildrenOnlyCall, isChildrenToArray, isChildrenToArrayCall, isClassComponent, isCloneElement, isCloneElementCall, isComponentDefinition, isComponentDidCatch, isComponentDidMount, isComponentDidMountCallback, isComponentDidUpdate, isComponentName, isComponentNameLoose, isComponentWillMount, isComponentWillReceiveProps, isComponentWillUnmount, isComponentWillUnmountCallback, isComponentWillUpdate, isComponentWrapperCall, isComponentWrapperCallLoose, isComponentWrapperCallback, isComponentWrapperCallbackLoose, isCreateContext, isCreateContextCall, isCreateElement, isCreateElementCall, isCreateRef, isCreateRefCall, isForwardRef, isForwardRefCall, isFunctionWithLooseComponentName, isGetChildContext, isGetDefaultProps, isGetDerivedStateFromError, isGetDerivedStateFromProps, isGetInitialState, isGetSnapshotBeforeUpdate, isHook, isHookCall, isHookCallWithName, isHookId, isHookName, isInitializedFromReact, isInitializedFromReactNative, isInitializedFromRef, isJsxLike, isLazy, isLazyCall, isMemo, isMemoCall, isPureComponent, isReactAPI, isReactAPICall, isRefId, isRefLikeName, isRender, isRenderMethodCallback, isRenderMethodLike, isShouldComponentUpdate, isThisSetState, isUnsafeComponentWillMount, isUnsafeComponentWillReceiveProps, isUnsafeComponentWillUpdate, isUseActionStateCall, isUseCall, isUseCallbackCall, isUseContextCall, isUseDebugValueCall, isUseDeferredValueCall, isUseEffectCall, isUseEffectCleanupCallback, isUseEffectLikeCall, isUseEffectSetupCallback, isUseFormStatusCall, isUseIdCall, isUseImperativeHandleCall, isUseInsertionEffectCall, isUseLayoutEffectCall, isUseMemoCall, isUseOptimisticCall, isUseReducerCall, isUseRefCall, isUseStateCall, isUseStateLikeCall, isUseSyncExternalStoreCall, isUseTransitionCall, useComponentCollector, useComponentCollectorLegacy, useHookCollector };
|