@eslint-react/core 3.0.0-next.7 → 3.0.0-next.70
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 +344 -318
- package/dist/index.js +607 -595
- 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,228 +482,398 @@ 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
|
-
*
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
*
|
|
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
|
|
690
|
-
/**
|
|
691
|
-
* Check if a node is a React class component
|
|
692
|
-
* @param node The AST node to check
|
|
693
|
-
* @returns `true` if the node is a class component, `false` otherwise
|
|
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
|
+
* ```
|
|
694
543
|
*/
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
+
};
|
|
702
555
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
function isPureComponent(node) {
|
|
711
|
-
if ("superClass" in node && node.superClass != null) {
|
|
712
|
-
const re = /^PureComponent$/u;
|
|
713
|
-
switch (true) {
|
|
714
|
-
case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
|
|
715
|
-
case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
|
|
716
|
-
}
|
|
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;
|
|
717
563
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Get
|
|
772
|
-
*
|
|
773
|
-
* @param node The
|
|
774
|
-
* @returns The
|
|
775
|
-
*/
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
+
* @param initialScope An optional scope to use for resolving spread attributes. If not provided,
|
|
600
|
+
*/
|
|
601
|
+
findAttribute(node, name, initialScope) {
|
|
602
|
+
initialScope ?? this.context.sourceCode.getScope(node);
|
|
603
|
+
return node.openingElement.attributes.findLast((attr) => {
|
|
604
|
+
if (attr.type === AST_NODE_TYPES.JSXAttribute) return stringifyJsx(attr.name) === name;
|
|
605
|
+
switch (attr.argument.type) {
|
|
606
|
+
case AST_NODE_TYPES.Identifier: {
|
|
607
|
+
const initNode = resolve(this.context, attr.argument);
|
|
608
|
+
if (initNode?.type === AST_NODE_TYPES.ObjectExpression) return ast.findProperty(initNode.properties, name) != null;
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
case AST_NODE_TYPES.ObjectExpression: return ast.findProperty(attr.argument.properties, name) != null;
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Get the stringified name of a `JSXAttribute` node
|
|
618
|
+
* (e.g. `"className"`, `"aria-label"`, `"xml:space"`).
|
|
619
|
+
* @param node The `JSXAttribute` node to extract the name from.
|
|
620
|
+
* @returns The stringified name of the attribute.
|
|
621
|
+
*/
|
|
622
|
+
getAttributeName(node) {
|
|
623
|
+
return stringifyJsx(node.name);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Resolve the static value of an attribute, automatically handling the
|
|
627
|
+
* `spreadProps` case by extracting the named property.
|
|
628
|
+
*
|
|
629
|
+
* This eliminates the repetitive pattern:
|
|
630
|
+
* ```ts
|
|
631
|
+
* const v = core.resolveJsxAttributeValue(ctx, attr);
|
|
632
|
+
* const s = v.kind === "spreadProps" ? v.getProperty(name) : v.toStatic();
|
|
633
|
+
* ```
|
|
634
|
+
*
|
|
635
|
+
* Returns `undefined` when the attribute is not present or its value
|
|
636
|
+
* cannot be statically determined.
|
|
637
|
+
* @param node The JSX element to search for the attribute.
|
|
638
|
+
* @param name The name of the attribute to resolve (e.g. `"className"`).
|
|
639
|
+
* @param initialScope An optional scope to use for resolving spread attributes. If not provided, the scope will be determined from the context of the attribute node.
|
|
640
|
+
* @returns The static value of the attribute, or `undefined` if not found or not statically resolvable.
|
|
641
|
+
*/
|
|
642
|
+
getAttributeStaticValue(node, name, initialScope) {
|
|
643
|
+
const attr = this.findAttribute(node, name, initialScope);
|
|
644
|
+
if (attr == null) return void 0;
|
|
645
|
+
const resolved = this.resolveAttributeValue(attr);
|
|
646
|
+
if (resolved.kind === "spreadProps") return resolved.getProperty(name);
|
|
647
|
+
return resolved.toStatic();
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* **All-in-one helper** – find an attribute by name on an element *and*
|
|
651
|
+
* resolve its value in a single call.
|
|
652
|
+
*
|
|
653
|
+
* Returns `undefined` when the attribute is not present.
|
|
654
|
+
* @param node The JSX element to search for the attribute.
|
|
655
|
+
* @param name The name of the attribute to find and resolve (e.g. `"className"`).
|
|
656
|
+
* @param initialScope An optional scope to use for resolving spread attributes. If not provided, the scope will be determined from the context of the attribute node.
|
|
657
|
+
* @returns A descriptor of the attribute's value that can be further inspected, or `undefined` if the attribute is not found.
|
|
658
|
+
*/
|
|
659
|
+
getAttributeValue(node, name, initialScope) {
|
|
660
|
+
const attr = this.findAttribute(node, name, initialScope);
|
|
661
|
+
if (attr == null) return void 0;
|
|
662
|
+
return this.resolveAttributeValue(attr);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Get the **self name** (last segment) of a JSX element type.
|
|
666
|
+
*
|
|
667
|
+
* - `<Foo.Bar.Baz>` → `"Baz"`
|
|
668
|
+
* - `<div>` → `"div"`
|
|
669
|
+
* - `<></>` → `""`
|
|
670
|
+
* @param node The JSX element or fragment to extract the self name from.
|
|
671
|
+
*/
|
|
672
|
+
getElementSelfName(node) {
|
|
673
|
+
return this.getElementType(node).split(".").at(-1) ?? "";
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Get the string representation of a JSX element's type.
|
|
677
|
+
*
|
|
678
|
+
* - `<div>` → `"div"`
|
|
679
|
+
* - `<Foo.Bar>` → `"Foo.Bar"`
|
|
680
|
+
* - `<React.Fragment>` → `"React.Fragment"`
|
|
681
|
+
* - `<></>` (JSXFragment) → `""`
|
|
682
|
+
* @param node The JSX element or fragment to extract the type from.
|
|
683
|
+
*/
|
|
684
|
+
getElementType(node) {
|
|
685
|
+
if (node.type === AST_NODE_TYPES.JSXFragment) return "";
|
|
686
|
+
return stringifyJsx(node.openingElement.name);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Shorthand: check whether an attribute exists on the element.
|
|
690
|
+
* @param node The JSX element to check for the attribute.
|
|
691
|
+
* @param name The name of the attribute to check for (e.g. `"className"`).
|
|
692
|
+
* @param initialScope An optional scope to use for resolving spread attributes. If not provided, the scope will be determined from the context of the attribute node.
|
|
693
|
+
* @returns `true` if the attribute exists on the element, `false` otherwise.
|
|
694
|
+
*/
|
|
695
|
+
hasAttribute(node, name, initialScope) {
|
|
696
|
+
return this.findAttribute(node, name, initialScope) != null;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Whether the node is a React **Fragment** element (either `<Fragment>` /
|
|
700
|
+
* `<React.Fragment>` or the shorthand `<>` syntax).
|
|
701
|
+
*
|
|
702
|
+
* The check honours the configured `jsxFragmentFactory`.
|
|
703
|
+
* @param node The node to check.
|
|
704
|
+
*/
|
|
705
|
+
isFragmentElement(node) {
|
|
706
|
+
if (node.type === AST_NODE_TYPES.JSXFragment) return true;
|
|
707
|
+
if (node.type !== AST_NODE_TYPES.JSXElement) return false;
|
|
708
|
+
const fragment = this.jsxConfig.jsxFragmentFactory.split(".").at(-1) ?? "Fragment";
|
|
709
|
+
return this.getElementType(node).split(".").at(-1) === fragment;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Whether the node is a **host** (intrinsic / DOM) element – i.e. its tag
|
|
713
|
+
* name starts with a lowercase letter.
|
|
714
|
+
* @param node The node to check.
|
|
715
|
+
*/
|
|
716
|
+
isHostElement(node) {
|
|
717
|
+
return node.type === AST_NODE_TYPES.JSXElement && node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && /^[a-z]/u.test(node.openingElement.name.name);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Resolve the *value* of a JSX attribute (or spread attribute) into a
|
|
721
|
+
* descriptor that can be inspected further.
|
|
722
|
+
*
|
|
723
|
+
* See {@link JsxAttributeValue} for the full set of `kind` discriminants.
|
|
724
|
+
* @param attribute The attribute node to resolve the value of.
|
|
725
|
+
* @returns A descriptor of the attribute's value that can be further inspected.
|
|
726
|
+
*/
|
|
727
|
+
resolveAttributeValue(attribute) {
|
|
728
|
+
const initialScope = this.context.sourceCode.getScope(attribute);
|
|
729
|
+
if (attribute.type === AST_NODE_TYPES.JSXAttribute) return this.#resolveJsxAttribute(attribute, initialScope);
|
|
730
|
+
return this.#resolveJsxSpreadAttribute(attribute, initialScope);
|
|
731
|
+
}
|
|
732
|
+
#resolveJsxAttribute(node, initialScope) {
|
|
733
|
+
if (node.value == null) return {
|
|
734
|
+
kind: "boolean",
|
|
735
|
+
toStatic() {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
switch (node.value.type) {
|
|
740
|
+
case AST_NODE_TYPES.Literal: {
|
|
741
|
+
const staticValue = node.value.value;
|
|
742
|
+
return {
|
|
743
|
+
kind: "literal",
|
|
744
|
+
node: node.value,
|
|
745
|
+
toStatic() {
|
|
746
|
+
return staticValue;
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
case AST_NODE_TYPES.JSXExpressionContainer: {
|
|
751
|
+
const expr = node.value.expression;
|
|
752
|
+
if (expr.type === AST_NODE_TYPES.JSXEmptyExpression) return {
|
|
753
|
+
kind: "missing",
|
|
754
|
+
node: expr,
|
|
755
|
+
toStatic() {
|
|
756
|
+
return "{}";
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
return {
|
|
760
|
+
kind: "expression",
|
|
761
|
+
node: expr,
|
|
762
|
+
toStatic() {
|
|
763
|
+
return getStaticValue(expr, initialScope)?.value;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
case AST_NODE_TYPES.JSXElement: return {
|
|
768
|
+
kind: "element",
|
|
769
|
+
node: node.value,
|
|
770
|
+
toStatic() {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
case AST_NODE_TYPES.JSXSpreadChild: {
|
|
775
|
+
const expr = node.value.expression;
|
|
776
|
+
return {
|
|
777
|
+
kind: "spreadChild",
|
|
778
|
+
getChildren(_at) {
|
|
779
|
+
return null;
|
|
780
|
+
},
|
|
781
|
+
node: node.value.expression,
|
|
782
|
+
toStatic() {
|
|
783
|
+
return getStaticValue(expr, initialScope)?.value;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
#resolveJsxSpreadAttribute(node, initialScope) {
|
|
790
|
+
return {
|
|
791
|
+
kind: "spreadProps",
|
|
792
|
+
getProperty(name) {
|
|
793
|
+
return match(getStaticValue(node.argument, initialScope)?.value).with({ [name]: P.select(P.any) }, identity).otherwise(() => null);
|
|
794
|
+
},
|
|
795
|
+
node: node.argument,
|
|
796
|
+
toStatic() {
|
|
797
|
+
return getStaticValue(node.argument, initialScope)?.value;
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
};
|
|
784
802
|
|
|
785
803
|
//#endregion
|
|
786
|
-
//#region src/component/component-
|
|
804
|
+
//#region src/component/component-detection-legacy.ts
|
|
787
805
|
/**
|
|
788
|
-
* Check if a
|
|
789
|
-
* @param
|
|
806
|
+
* Check if a node is a React class component
|
|
807
|
+
* @param node The AST node to check
|
|
808
|
+
* @returns `true` if the node is a class component, `false` otherwise
|
|
790
809
|
*/
|
|
791
|
-
function
|
|
792
|
-
|
|
810
|
+
function isClassComponent(node) {
|
|
811
|
+
if ("superClass" in node && node.superClass != null) {
|
|
812
|
+
const re = /^(?:Pure)?Component$/u;
|
|
813
|
+
switch (true) {
|
|
814
|
+
case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
|
|
815
|
+
case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return false;
|
|
793
819
|
}
|
|
794
820
|
/**
|
|
795
|
-
* Check if a
|
|
796
|
-
* @param
|
|
821
|
+
* Check if a node is a React PureComponent
|
|
822
|
+
* @param node The AST node to check
|
|
823
|
+
* @returns `true` if the node is a PureComponent, `false` otherwise
|
|
797
824
|
*/
|
|
798
|
-
function
|
|
799
|
-
|
|
825
|
+
function isPureComponent(node) {
|
|
826
|
+
if ("superClass" in node && node.superClass != null) {
|
|
827
|
+
const re = /^PureComponent$/u;
|
|
828
|
+
switch (true) {
|
|
829
|
+
case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
|
|
830
|
+
case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return false;
|
|
800
834
|
}
|
|
801
835
|
/**
|
|
802
|
-
*
|
|
803
|
-
* @param
|
|
804
|
-
* @param
|
|
805
|
-
* @param allowNone Whether to allow no name
|
|
806
|
-
* @returns Whether the function has a loose component name
|
|
836
|
+
* Create a lifecycle method checker function
|
|
837
|
+
* @param methodName The lifecycle method name
|
|
838
|
+
* @param isStatic Whether the method is static
|
|
807
839
|
*/
|
|
808
|
-
function
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
840
|
+
function createLifecycleChecker(methodName, isStatic = false) {
|
|
841
|
+
return (node) => ast.isMethodOrProperty(node) && node.static === isStatic && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === methodName;
|
|
842
|
+
}
|
|
843
|
+
const isRender = createLifecycleChecker("render");
|
|
844
|
+
const isComponentDidCatch = createLifecycleChecker("componentDidCatch");
|
|
845
|
+
const isComponentDidMount = createLifecycleChecker("componentDidMount");
|
|
846
|
+
const isComponentDidUpdate = createLifecycleChecker("componentDidUpdate");
|
|
847
|
+
const isComponentWillMount = createLifecycleChecker("componentWillMount");
|
|
848
|
+
const isComponentWillReceiveProps = createLifecycleChecker("componentWillReceiveProps");
|
|
849
|
+
const isComponentWillUnmount = createLifecycleChecker("componentWillUnmount");
|
|
850
|
+
const isComponentWillUpdate = createLifecycleChecker("componentWillUpdate");
|
|
851
|
+
const isGetChildContext = createLifecycleChecker("getChildContext");
|
|
852
|
+
const isGetInitialState = createLifecycleChecker("getInitialState");
|
|
853
|
+
const isGetSnapshotBeforeUpdate = createLifecycleChecker("getSnapshotBeforeUpdate");
|
|
854
|
+
const isShouldComponentUpdate = createLifecycleChecker("shouldComponentUpdate");
|
|
855
|
+
const isUnsafeComponentWillMount = createLifecycleChecker("UNSAFE_componentWillMount");
|
|
856
|
+
const isUnsafeComponentWillReceiveProps = createLifecycleChecker("UNSAFE_componentWillReceiveProps");
|
|
857
|
+
const isUnsafeComponentWillUpdate = createLifecycleChecker("UNSAFE_componentWillUpdate");
|
|
858
|
+
const isGetDefaultProps = createLifecycleChecker("getDefaultProps", true);
|
|
859
|
+
const isGetDerivedStateFromProps = createLifecycleChecker("getDerivedStateFromProps", true);
|
|
860
|
+
const isGetDerivedStateFromError = createLifecycleChecker("getDerivedStateFromError", true);
|
|
861
|
+
/**
|
|
862
|
+
* Check if the given node is a componentDidMount callback
|
|
863
|
+
* @param node The node to check
|
|
864
|
+
* @returns True if the node is a componentDidMount callback, false otherwise
|
|
865
|
+
*/
|
|
866
|
+
function isComponentDidMountCallback(node) {
|
|
867
|
+
return ast.isFunction(node) && isComponentDidMount(node.parent) && node.parent.value === node;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Check if the given node is a componentWillUnmount callback
|
|
871
|
+
* @param node The node to check
|
|
872
|
+
* @returns True if the node is a componentWillUnmount callback, false otherwise
|
|
873
|
+
*/
|
|
874
|
+
function isComponentWillUnmountCallback(node) {
|
|
875
|
+
return ast.isFunction(node) && isComponentWillUnmount(node.parent) && node.parent.value === node;
|
|
814
876
|
}
|
|
815
|
-
|
|
816
|
-
//#endregion
|
|
817
|
-
//#region src/component/component-render-method.ts
|
|
818
877
|
/**
|
|
819
878
|
* Check whether given node is a render method of a class component
|
|
820
879
|
* @example
|
|
@@ -830,11 +889,8 @@ function isFunctionWithLooseComponentName(context, fn, allowNone = false) {
|
|
|
830
889
|
function isRenderMethodLike(node) {
|
|
831
890
|
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
891
|
}
|
|
833
|
-
|
|
834
|
-
//#endregion
|
|
835
|
-
//#region src/component/component-definition.ts
|
|
836
892
|
/**
|
|
837
|
-
* Check if the given node is a function within a render method of a class component
|
|
893
|
+
* Check if the given node is a function within a render method of a class component
|
|
838
894
|
*
|
|
839
895
|
* @param node The AST node to check
|
|
840
896
|
* @returns `true` if the node is a render function inside a class component
|
|
@@ -852,38 +908,139 @@ function isRenderMethodCallback(node) {
|
|
|
852
908
|
return greatGrandparent != null && isRenderMethodLike(parent) && isClassComponent(greatGrandparent);
|
|
853
909
|
}
|
|
854
910
|
/**
|
|
855
|
-
* Check
|
|
856
|
-
*
|
|
857
|
-
* @
|
|
858
|
-
* @param hint Component detection hints as bit flags
|
|
859
|
-
* @returns `true` if the function matches an exclusion hint
|
|
911
|
+
* Check whether the given node is a this.setState() call
|
|
912
|
+
* @param node The node to check
|
|
913
|
+
* @internal
|
|
860
914
|
*/
|
|
861
|
-
function
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassMethod && ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.MethodDefinition: return true;
|
|
865
|
-
case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassProperty && ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.Property: return true;
|
|
866
|
-
case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayPattern && node.parent.type === AST_NODE_TYPES.ArrayPattern: return true;
|
|
867
|
-
case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayExpression && node.parent.type === AST_NODE_TYPES.ArrayExpression: return true;
|
|
868
|
-
case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback && 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": return true;
|
|
869
|
-
case hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback && 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": return true;
|
|
870
|
-
}
|
|
871
|
-
return false;
|
|
915
|
+
function isThisSetState(node) {
|
|
916
|
+
const { callee } = node;
|
|
917
|
+
return callee.type === AST_NODE_TYPES.MemberExpression && ast.isThisExpressionLoose(callee.object) && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "setState";
|
|
872
918
|
}
|
|
873
919
|
/**
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
* @
|
|
877
|
-
|
|
878
|
-
|
|
920
|
+
* Check whether the given node is an assignment to this.state
|
|
921
|
+
* @param node The node to check
|
|
922
|
+
* @internal
|
|
923
|
+
*/
|
|
924
|
+
function isAssignmentToThisState(node) {
|
|
925
|
+
const { left } = node;
|
|
926
|
+
return left.type === AST_NODE_TYPES.MemberExpression && ast.isThisExpressionLoose(left.object) && ast.getPropertyName(left.property) === "state";
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/component/component-wrapper.ts
|
|
931
|
+
/**
|
|
932
|
+
* Check if the node is a call expression for a component wrapper
|
|
933
|
+
* @param context The ESLint rule context
|
|
934
|
+
* @param node The node to check
|
|
935
|
+
* @returns `true` if the node is a call expression for a component wrapper
|
|
936
|
+
*/
|
|
937
|
+
function isComponentWrapperCall(context, node) {
|
|
938
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
939
|
+
return isMemoCall(context, node) || isForwardRefCall(context, node);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Check if the node is a call expression for a component wrapper loosely
|
|
943
|
+
* @param context The ESLint rule context
|
|
944
|
+
* @param node The node to check
|
|
945
|
+
* @returns `true` if the node is a call expression for a component wrapper loosely
|
|
946
|
+
*/
|
|
947
|
+
function isComponentWrapperCallLoose(context, node) {
|
|
948
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
949
|
+
return isComponentWrapperCall(context, node) || isUseCallbackCall(node);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Check if the node is a callback function passed to a component wrapper
|
|
953
|
+
* @param context The ESLint rule context
|
|
954
|
+
* @param node The node to check
|
|
955
|
+
* @returns `true` if the node is a callback function passed to a component wrapper
|
|
879
956
|
*/
|
|
880
|
-
function
|
|
957
|
+
function isComponentWrapperCallback(context, node) {
|
|
958
|
+
if (!ast.isFunction(node)) return false;
|
|
881
959
|
const parent = node.parent;
|
|
882
|
-
if (parent
|
|
883
|
-
|
|
884
|
-
|
|
960
|
+
if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
961
|
+
return isComponentWrapperCall(context, parent);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Check if the node is a callback function passed to a component wrapper loosely
|
|
965
|
+
* @param context The ESLint rule context
|
|
966
|
+
* @param node The node to check
|
|
967
|
+
* @returns `true` if the node is a callback function passed to a component wrapper loosely
|
|
968
|
+
*/
|
|
969
|
+
function isComponentWrapperCallbackLoose(context, node) {
|
|
970
|
+
if (!ast.isFunction(node)) return false;
|
|
971
|
+
const parent = node.parent;
|
|
972
|
+
if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
973
|
+
return isComponentWrapperCallLoose(context, parent);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
//#endregion
|
|
977
|
+
//#region src/component/component-id.ts
|
|
978
|
+
/**
|
|
979
|
+
* Get function component identifier from `const Component = memo(() => {});`
|
|
980
|
+
* @param context The rule context
|
|
981
|
+
* @param node The function node to analyze
|
|
982
|
+
* @returns The function identifier or `null` if not found
|
|
983
|
+
*/
|
|
984
|
+
function getFunctionComponentId(context, node) {
|
|
985
|
+
const functionId = ast.getFunctionId(node);
|
|
986
|
+
if (functionId != null) return functionId;
|
|
987
|
+
const { parent } = node;
|
|
988
|
+
if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.id;
|
|
989
|
+
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;
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
//#endregion
|
|
994
|
+
//#region src/component/component-name.ts
|
|
995
|
+
/**
|
|
996
|
+
* Check if a string matches the strict component name pattern
|
|
997
|
+
* @param name The name to check
|
|
998
|
+
*/
|
|
999
|
+
function isComponentName(name) {
|
|
1000
|
+
return RE_COMPONENT_NAME.test(name);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Check if a string matches the loose component name pattern
|
|
1004
|
+
* @param name The name to check
|
|
1005
|
+
*/
|
|
1006
|
+
function isComponentNameLoose(name) {
|
|
1007
|
+
return RE_COMPONENT_NAME_LOOSE.test(name);
|
|
885
1008
|
}
|
|
886
1009
|
/**
|
|
1010
|
+
* Check if a function has a loose component name
|
|
1011
|
+
* @param context The rule context
|
|
1012
|
+
* @param fn The function to check
|
|
1013
|
+
* @param allowNone Whether to allow no name
|
|
1014
|
+
* @returns Whether the function has a loose component name
|
|
1015
|
+
*/
|
|
1016
|
+
function isFunctionWithLooseComponentName(context, fn, allowNone = false) {
|
|
1017
|
+
const id = getFunctionComponentId(context, fn);
|
|
1018
|
+
if (id == null) return allowNone;
|
|
1019
|
+
if (id.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.name);
|
|
1020
|
+
if (id.type === AST_NODE_TYPES.MemberExpression && id.property.type === AST_NODE_TYPES.Identifier) return isComponentNameLoose(id.property.name);
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
//#endregion
|
|
1025
|
+
//#region src/component/component-detection.ts
|
|
1026
|
+
/**
|
|
1027
|
+
* Hints for component collector
|
|
1028
|
+
*/
|
|
1029
|
+
const ComponentDetectionHint = {
|
|
1030
|
+
...JsxDetectionHint,
|
|
1031
|
+
DoNotIncludeFunctionDefinedAsArrayFlatMapCallback: 1n << 17n,
|
|
1032
|
+
DoNotIncludeFunctionDefinedAsArrayMapCallback: 1n << 16n,
|
|
1033
|
+
DoNotIncludeFunctionDefinedInArrayExpression: 1n << 15n,
|
|
1034
|
+
DoNotIncludeFunctionDefinedInArrayPattern: 1n << 14n,
|
|
1035
|
+
DoNotIncludeFunctionDefinedOnClassMethod: 1n << 12n,
|
|
1036
|
+
DoNotIncludeFunctionDefinedOnClassProperty: 1n << 13n,
|
|
1037
|
+
DoNotIncludeFunctionDefinedOnObjectMethod: 1n << 11n
|
|
1038
|
+
};
|
|
1039
|
+
/**
|
|
1040
|
+
* Default component detection hint
|
|
1041
|
+
*/
|
|
1042
|
+
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;
|
|
1043
|
+
/**
|
|
887
1044
|
* Determine if a function node represents a valid React component definition
|
|
888
1045
|
*
|
|
889
1046
|
* @param context The rule context
|
|
@@ -893,8 +1050,33 @@ function isChildrenOfCreateElement(context, node) {
|
|
|
893
1050
|
*/
|
|
894
1051
|
function isComponentDefinition(context, node, hint) {
|
|
895
1052
|
if (!isFunctionWithLooseComponentName(context, node, true)) return false;
|
|
896
|
-
|
|
897
|
-
|
|
1053
|
+
switch (true) {
|
|
1054
|
+
case node.parent.type === AST_NODE_TYPES.CallExpression && isCreateElementCall(context, node.parent) && node.parent.arguments.slice(2).some((arg) => arg === node): return false;
|
|
1055
|
+
case isRenderMethodCallback(node): return false;
|
|
1056
|
+
}
|
|
1057
|
+
switch (true) {
|
|
1058
|
+
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:
|
|
1059
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnObjectMethod) return false;
|
|
1060
|
+
break;
|
|
1061
|
+
case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.MethodDefinition:
|
|
1062
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassMethod) return false;
|
|
1063
|
+
break;
|
|
1064
|
+
case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && node.parent.type === AST_NODE_TYPES.Property:
|
|
1065
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedOnClassProperty) return false;
|
|
1066
|
+
break;
|
|
1067
|
+
case node.parent.type === AST_NODE_TYPES.ArrayPattern:
|
|
1068
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayPattern) return false;
|
|
1069
|
+
break;
|
|
1070
|
+
case node.parent.type === AST_NODE_TYPES.ArrayExpression:
|
|
1071
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedInArrayExpression) return false;
|
|
1072
|
+
break;
|
|
1073
|
+
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":
|
|
1074
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback) return false;
|
|
1075
|
+
break;
|
|
1076
|
+
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":
|
|
1077
|
+
if (hint & ComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback) return false;
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
898
1080
|
const significantParent = ast.findParentNode(node, ast.isOneOf([
|
|
899
1081
|
AST_NODE_TYPES.JSXExpressionContainer,
|
|
900
1082
|
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
@@ -913,15 +1095,12 @@ function isComponentDefinition(context, node, hint) {
|
|
|
913
1095
|
* Component flag constants
|
|
914
1096
|
*/
|
|
915
1097
|
const ComponentFlag = {
|
|
916
|
-
None: 0n,
|
|
917
|
-
PureComponent: 1n << 0n,
|
|
918
1098
|
CreateElement: 1n << 1n,
|
|
1099
|
+
ForwardRef: 1n << 3n,
|
|
919
1100
|
Memo: 1n << 2n,
|
|
920
|
-
|
|
1101
|
+
None: 0n,
|
|
1102
|
+
PureComponent: 1n << 0n
|
|
921
1103
|
};
|
|
922
|
-
|
|
923
|
-
//#endregion
|
|
924
|
-
//#region src/component/component-init-path.ts
|
|
925
1104
|
/**
|
|
926
1105
|
* Get component flag from init path
|
|
927
1106
|
* @param initPath The init path of the function component
|
|
@@ -936,7 +1115,7 @@ function getComponentFlagFromInitPath(initPath) {
|
|
|
936
1115
|
|
|
937
1116
|
//#endregion
|
|
938
1117
|
//#region src/component/component-collector.ts
|
|
939
|
-
const idGen$1 = new IdGenerator("
|
|
1118
|
+
const idGen$1 = new IdGenerator("function-component:");
|
|
940
1119
|
/**
|
|
941
1120
|
* Get a ctx and visitor object for the rule to collect function components
|
|
942
1121
|
* @param context The ESLint rule context
|
|
@@ -948,14 +1127,14 @@ function useComponentCollector(context, options = {}) {
|
|
|
948
1127
|
const functionEntries = [];
|
|
949
1128
|
const components = /* @__PURE__ */ new Map();
|
|
950
1129
|
const getText = (n) => context.sourceCode.getText(n);
|
|
951
|
-
const getCurrentEntry = () => functionEntries.at(-1);
|
|
1130
|
+
const getCurrentEntry = () => functionEntries.at(-1) ?? null;
|
|
952
1131
|
const onFunctionEnter = (node) => {
|
|
953
1132
|
const key = idGen$1.next();
|
|
954
1133
|
const exp = ast.findParentNode(node, (n) => n.type === AST_NODE_TYPES.ExportDefaultDeclaration);
|
|
955
1134
|
const isExportDefault = exp != null;
|
|
956
1135
|
const isExportDefaultDeclaration = exp != null && ast.getUnderlyingExpression(exp.declaration) === node;
|
|
957
1136
|
const id = getFunctionComponentId(context, node);
|
|
958
|
-
const name = id == null ?
|
|
1137
|
+
const name = id == null ? null : ast.getFullyQualifiedName(id, getText);
|
|
959
1138
|
const initPath = ast.getFunctionInitPath(node);
|
|
960
1139
|
const directives = ast.getFunctionDirectives(node);
|
|
961
1140
|
const entry = {
|
|
@@ -963,9 +1142,8 @@ function useComponentCollector(context, options = {}) {
|
|
|
963
1142
|
key,
|
|
964
1143
|
kind: "function-component",
|
|
965
1144
|
name,
|
|
966
|
-
node,
|
|
967
1145
|
directives,
|
|
968
|
-
displayName:
|
|
1146
|
+
displayName: null,
|
|
969
1147
|
flag: getComponentFlagFromInitPath(initPath),
|
|
970
1148
|
hint,
|
|
971
1149
|
hookCalls: [],
|
|
@@ -973,6 +1151,7 @@ function useComponentCollector(context, options = {}) {
|
|
|
973
1151
|
isComponentDefinition: isComponentDefinition(context, node, hint),
|
|
974
1152
|
isExportDefault,
|
|
975
1153
|
isExportDefaultDeclaration,
|
|
1154
|
+
node,
|
|
976
1155
|
rets: []
|
|
977
1156
|
};
|
|
978
1157
|
functionEntries.push(entry);
|
|
@@ -1002,13 +1181,13 @@ function useComponentCollector(context, options = {}) {
|
|
|
1002
1181
|
if (body.type === AST_NODE_TYPES.BlockStatement) return;
|
|
1003
1182
|
entry.rets.push(body);
|
|
1004
1183
|
if (!entry.isComponentDefinition) return;
|
|
1005
|
-
if (!components.has(entry.key) && !isJsxLike(context
|
|
1184
|
+
if (!components.has(entry.key) && !isJsxLike(context, body, hint)) return;
|
|
1006
1185
|
components.set(entry.key, entry);
|
|
1007
1186
|
},
|
|
1008
1187
|
...collectDisplayName ? { [ast.SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION](node) {
|
|
1009
1188
|
const { left, right } = node;
|
|
1010
1189
|
if (left.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1011
|
-
const componentName = left.object.type === AST_NODE_TYPES.Identifier ? left.object.name :
|
|
1190
|
+
const componentName = left.object.type === AST_NODE_TYPES.Identifier ? left.object.name : null;
|
|
1012
1191
|
const component = [...components.values()].findLast(({ name }) => name != null && name === componentName);
|
|
1013
1192
|
if (component == null) return;
|
|
1014
1193
|
component.displayName = right;
|
|
@@ -1027,7 +1206,7 @@ function useComponentCollector(context, options = {}) {
|
|
|
1027
1206
|
entry.rets.push(node.argument);
|
|
1028
1207
|
if (!entry.isComponentDefinition) return;
|
|
1029
1208
|
const { argument } = node;
|
|
1030
|
-
if (!components.has(entry.key) && !isJsxLike(context
|
|
1209
|
+
if (!components.has(entry.key) && !isJsxLike(context, argument, hint)) return;
|
|
1031
1210
|
components.set(entry.key, entry);
|
|
1032
1211
|
}
|
|
1033
1212
|
}
|
|
@@ -1036,7 +1215,7 @@ function useComponentCollector(context, options = {}) {
|
|
|
1036
1215
|
|
|
1037
1216
|
//#endregion
|
|
1038
1217
|
//#region src/component/component-collector-legacy.ts
|
|
1039
|
-
const idGen = new IdGenerator("
|
|
1218
|
+
const idGen = new IdGenerator("class-component:");
|
|
1040
1219
|
/**
|
|
1041
1220
|
* Get a ctx and visitor object for the rule to collect class componentss
|
|
1042
1221
|
* @param context The ESLint rule context
|
|
@@ -1052,18 +1231,18 @@ function useComponentCollectorLegacy(context) {
|
|
|
1052
1231
|
if (!isClassComponent(node)) return;
|
|
1053
1232
|
const id = ast.getClassId(node);
|
|
1054
1233
|
const key = idGen.next();
|
|
1055
|
-
const name = id == null ?
|
|
1234
|
+
const name = id == null ? null : ast.getFullyQualifiedName(id, getText);
|
|
1056
1235
|
const flag = isPureComponent(node) ? ComponentFlag.PureComponent : ComponentFlag.None;
|
|
1057
1236
|
components.set(key, {
|
|
1058
1237
|
id,
|
|
1059
1238
|
key,
|
|
1060
1239
|
kind: "class-component",
|
|
1061
1240
|
name,
|
|
1062
|
-
|
|
1063
|
-
displayName: unit,
|
|
1241
|
+
displayName: null,
|
|
1064
1242
|
flag,
|
|
1065
1243
|
hint: 0n,
|
|
1066
|
-
methods: []
|
|
1244
|
+
methods: [],
|
|
1245
|
+
node
|
|
1067
1246
|
});
|
|
1068
1247
|
};
|
|
1069
1248
|
return {
|
|
@@ -1074,179 +1253,6 @@ function useComponentCollectorLegacy(context) {
|
|
|
1074
1253
|
}
|
|
1075
1254
|
};
|
|
1076
1255
|
}
|
|
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
1256
|
|
|
1251
1257
|
//#endregion
|
|
1252
1258
|
//#region src/ref/ref-name.ts
|
|
@@ -1255,12 +1261,18 @@ function isInsideComponentOrHook(node) {
|
|
|
1255
1261
|
* @param name The name to check
|
|
1256
1262
|
* @returns True if the name is "ref" or ends with "Ref"
|
|
1257
1263
|
*/
|
|
1258
|
-
function
|
|
1264
|
+
function isRefLikeName(name) {
|
|
1259
1265
|
return name === "ref" || name.endsWith("Ref");
|
|
1260
1266
|
}
|
|
1261
1267
|
|
|
1262
1268
|
//#endregion
|
|
1263
|
-
//#region src/ref/
|
|
1269
|
+
//#region src/ref/ref-id.ts
|
|
1270
|
+
function isRefId(node) {
|
|
1271
|
+
return node.type === AST_NODE_TYPES.Identifier && isRefLikeName(node.name);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
//#endregion
|
|
1275
|
+
//#region src/ref/ref-init.ts
|
|
1264
1276
|
/**
|
|
1265
1277
|
* Check if the variable with the given name is initialized or derived from a ref
|
|
1266
1278
|
* @param name The variable name
|
|
@@ -1274,20 +1286,20 @@ function isInitializedFromRef(name, initialScope) {
|
|
|
1274
1286
|
* Get the init expression of a ref variable
|
|
1275
1287
|
* @param name The variable name
|
|
1276
1288
|
* @param initialScope The initial scope
|
|
1277
|
-
* @returns The init expression node if the variable is derived from a ref, or
|
|
1289
|
+
* @returns The init expression node if the variable is derived from a ref, or null otherwise
|
|
1278
1290
|
*/
|
|
1279
1291
|
function getRefInit(name, initialScope) {
|
|
1280
|
-
for (const { node } of findVariable(initialScope
|
|
1292
|
+
for (const { node } of findVariable(initialScope, name)?.defs ?? []) {
|
|
1281
1293
|
if (node.type !== AST_NODE_TYPES$1.VariableDeclarator) continue;
|
|
1282
1294
|
const init = node.init;
|
|
1283
1295
|
if (init == null) continue;
|
|
1284
1296
|
switch (true) {
|
|
1285
|
-
case init.type === AST_NODE_TYPES$1.MemberExpression && init.object.type === AST_NODE_TYPES$1.Identifier &&
|
|
1297
|
+
case init.type === AST_NODE_TYPES$1.MemberExpression && init.object.type === AST_NODE_TYPES$1.Identifier && isRefLikeName(init.object.name): return init;
|
|
1286
1298
|
case init.type === AST_NODE_TYPES$1.CallExpression && isUseRefCall(init): return init;
|
|
1287
1299
|
}
|
|
1288
1300
|
}
|
|
1289
|
-
return
|
|
1301
|
+
return null;
|
|
1290
1302
|
}
|
|
1291
1303
|
|
|
1292
1304
|
//#endregion
|
|
1293
|
-
export { ComponentDetectionHint, ComponentFlag, DEFAULT_COMPONENT_DETECTION_HINT, DEFAULT_JSX_DETECTION_HINT, JsxDetectionHint, JsxEmit,
|
|
1305
|
+
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 };
|