react-doctor 0.0.8 → 0.0.10

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.
@@ -1,10 +1,10 @@
1
1
  //#region src/plugin/constants.ts
2
- const GIANT_COMPONENT_LINE_THRESHOLD = 200;
3
- const CASCADING_SET_STATE_THRESHOLD = 2;
4
- const RELATED_USE_STATE_THRESHOLD = 3;
2
+ const GIANT_COMPONENT_LINE_THRESHOLD = 300;
3
+ const CASCADING_SET_STATE_THRESHOLD = 3;
4
+ const RELATED_USE_STATE_THRESHOLD = 5;
5
5
  const DEEP_NESTING_THRESHOLD = 3;
6
6
  const DUPLICATE_STORAGE_READ_THRESHOLD = 2;
7
- const SEQUENTIAL_AWAIT_THRESHOLD = 2;
7
+ const SEQUENTIAL_AWAIT_THRESHOLD = 3;
8
8
  const SECRET_MIN_LENGTH_CHARS = 8;
9
9
  const AUTH_CHECK_LOOKAHEAD_STATEMENTS = 3;
10
10
  const LAYOUT_PROPERTIES = new Set([
@@ -109,16 +109,84 @@ const SECRET_PATTERNS = [
109
109
  /^sk-[a-zA-Z0-9]{32,}$/
110
110
  ];
111
111
  const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
112
- const LOADING_STATE_PATTERN = /(?:loading|isLoading|isPending|isSubmitting|isFetching)/i;
113
- const EVENT_PROP_PATTERN = /^on[A-Z]/;
112
+ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
113
+ "modal",
114
+ "label",
115
+ "text",
116
+ "title",
117
+ "name",
118
+ "id",
119
+ "key",
120
+ "url",
121
+ "path",
122
+ "route",
123
+ "page",
124
+ "param",
125
+ "field",
126
+ "column",
127
+ "header",
128
+ "placeholder",
129
+ "description",
130
+ "type",
131
+ "icon",
132
+ "class",
133
+ "style",
134
+ "variant",
135
+ "event",
136
+ "action",
137
+ "status",
138
+ "state",
139
+ "mode",
140
+ "flag",
141
+ "option",
142
+ "config",
143
+ "message",
144
+ "error",
145
+ "display",
146
+ "view",
147
+ "component",
148
+ "element",
149
+ "container",
150
+ "wrapper",
151
+ "button",
152
+ "link",
153
+ "input",
154
+ "select",
155
+ "dialog",
156
+ "menu",
157
+ "form",
158
+ "step",
159
+ "index",
160
+ "count",
161
+ "length",
162
+ "role",
163
+ "scope",
164
+ "context",
165
+ "provider",
166
+ "ref",
167
+ "handler",
168
+ "query",
169
+ "schema",
170
+ "constant"
171
+ ]);
172
+ const TRIVIAL_INITIALIZER_NAMES = new Set([
173
+ "Boolean",
174
+ "String",
175
+ "Number",
176
+ "Array",
177
+ "Object",
178
+ "parseInt",
179
+ "parseFloat"
180
+ ]);
114
181
  const SETTER_PATTERN = /^set[A-Z]/;
115
182
  const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
116
183
  const UPPERCASE_PATTERN = /^[A-Z]/;
117
184
  const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
118
185
  const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
186
+ const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
187
+ const TEST_FILE_PATTERN = /\.(?:test|spec|stories)\.[tj]sx?$/;
188
+ const OG_ROUTE_PATTERN = /\/og\b/i;
119
189
  const PAGES_DIRECTORY_PATTERN = /\/pages\//;
120
- const SERVER_ACTION_FILE_PATTERN = /actions?\.(tsx?|jsx?)$/;
121
- const SERVER_ACTION_DIRECTORY_PATTERN = /\/actions\//;
122
190
  const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
123
191
  "redirect",
124
192
  "permanentRedirect",
@@ -305,16 +373,6 @@ const extractDestructuredPropNames = (params) => {
305
373
 
306
374
  //#endregion
307
375
  //#region src/plugin/rules/architecture.ts
308
- const noGenericHandlerNames = { create: (context) => ({ JSXAttribute(node) {
309
- if (node.name?.type !== "JSXIdentifier" || !EVENT_PROP_PATTERN.test(node.name.name)) return;
310
- if (!node.value || node.value.type !== "JSXExpressionContainer") return;
311
- const mirroredHandlerName = `handle${node.name.name.slice(2)}`;
312
- const expression = node.value.expression;
313
- if (expression?.type === "Identifier" && expression.name === mirroredHandlerName) context.report({
314
- node,
315
- message: `Non-descriptive handler name "${expression.name}" — name should describe what it does, not when it runs`
316
- });
317
- } }) };
318
376
  const noGiantComponent = { create: (context) => {
319
377
  const reportOversizedComponent = (nameNode, componentName, bodyNode) => {
320
378
  if (!bodyNode.loc) return;
@@ -376,14 +434,21 @@ const noNestedComponentDefinition = { create: (context) => {
376
434
 
377
435
  //#endregion
378
436
  //#region src/plugin/rules/bundle-size.ts
379
- const noBarrelImport = { create: (context) => ({ ImportDeclaration(node) {
380
- const source = node.source?.value;
381
- if (typeof source !== "string" || !source.startsWith(".")) return;
382
- if (BARREL_INDEX_SUFFIXES.some((suffix) => source.endsWith(suffix))) context.report({
383
- node,
384
- message: "Import from barrel/index file import directly from the source module for better tree-shaking"
385
- });
386
- } }) };
437
+ const noBarrelImport = { create: (context) => {
438
+ let didReportForFile = false;
439
+ return { ImportDeclaration(node) {
440
+ if (didReportForFile) return;
441
+ const source = node.source?.value;
442
+ if (typeof source !== "string" || !source.startsWith(".")) return;
443
+ if (BARREL_INDEX_SUFFIXES.some((suffix) => source.endsWith(suffix))) {
444
+ didReportForFile = true;
445
+ context.report({
446
+ node,
447
+ message: "Import from barrel/index file — import directly from the source module for better tree-shaking"
448
+ });
449
+ }
450
+ } };
451
+ } };
387
452
  const noFullLodashImport = { create: (context) => ({ ImportDeclaration(node) {
388
453
  const source = node.source?.value;
389
454
  if (source === "lodash" || source === "lodash-es") context.report({
@@ -457,11 +522,28 @@ const extractIndexName = (node) => {
457
522
  if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "String" && node.arguments?.[0]?.type === "Identifier" && INDEX_PARAMETER_NAMES.has(node.arguments[0].name)) return node.arguments[0].name;
458
523
  return null;
459
524
  };
525
+ const isInsideStaticPlaceholderMap = (node) => {
526
+ let current = node;
527
+ while (current.parent) {
528
+ current = current.parent;
529
+ if (current.type === "CallExpression" && current.callee?.type === "MemberExpression" && current.callee.property?.name === "map") {
530
+ const receiver = current.callee.object;
531
+ if (receiver?.type === "CallExpression") {
532
+ const callee = receiver.callee;
533
+ if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "Array" && callee.property?.name === "from") return true;
534
+ }
535
+ if (receiver?.type === "NewExpression" && receiver.callee?.type === "Identifier" && receiver.callee.name === "Array") return true;
536
+ }
537
+ }
538
+ return false;
539
+ };
460
540
  const noArrayIndexAsKey = { create: (context) => ({ JSXAttribute(node) {
461
541
  if (node.name?.type !== "JSXIdentifier" || node.name.name !== "key") return;
462
542
  if (!node.value || node.value.type !== "JSXExpressionContainer") return;
463
543
  const indexName = extractIndexName(node.value.expression);
464
- if (indexName) context.report({
544
+ if (!indexName) return;
545
+ if (isInsideStaticPlaceholderMap(node)) return;
546
+ context.report({
465
547
  node,
466
548
  message: `Array index "${indexName}" used as key — causes bugs when list is reordered or filtered`
467
549
  });
@@ -488,7 +570,7 @@ const noPreventDefault = { create: (context) => ({ JSXOpeningElement(node) {
488
570
  const expression = eventAttribute.value.expression;
489
571
  if (expression?.type !== "ArrowFunctionExpression" && expression?.type !== "FunctionExpression") return;
490
572
  if (!containsPreventDefaultCall(expression)) return;
491
- const message = elementName === "form" ? "preventDefault() on <form> onSubmit — form won't work without JavaScript" : "preventDefault() on <a> onClick — use a <button> or routing component instead";
573
+ const message = elementName === "form" ? "preventDefault() on <form> onSubmit — form won't work without JavaScript. Consider using a server action for progressive enhancement" : "preventDefault() on <a> onClick — use a <button> or routing component instead";
492
574
  context.report({
493
575
  node,
494
576
  message
@@ -605,16 +687,21 @@ const jsEarlyExit = { create: (context) => ({ IfStatement(node) {
605
687
  message: `${nestingDepth + 1} levels of nested if statements — use early returns to flatten`
606
688
  });
607
689
  } }) };
608
- const asyncParallel = { create: (context) => ({ BlockStatement(node) {
609
- const consecutiveAwaitStatements = [];
610
- const flushConsecutiveAwaits = () => {
611
- if (consecutiveAwaitStatements.length >= SEQUENTIAL_AWAIT_THRESHOLD) reportIfIndependent(consecutiveAwaitStatements, context);
612
- consecutiveAwaitStatements.length = 0;
613
- };
614
- for (const statement of node.body ?? []) if (statement.type === "VariableDeclaration" && statement.declarations?.length === 1 && statement.declarations[0].init?.type === "AwaitExpression" || statement.type === "ExpressionStatement" && statement.expression?.type === "AwaitExpression") consecutiveAwaitStatements.push(statement);
615
- else flushConsecutiveAwaits();
616
- flushConsecutiveAwaits();
617
- } }) };
690
+ const asyncParallel = { create: (context) => {
691
+ const filename = context.getFilename?.() ?? "";
692
+ const isTestFile = TEST_FILE_PATTERN.test(filename);
693
+ return { BlockStatement(node) {
694
+ if (isTestFile) return;
695
+ const consecutiveAwaitStatements = [];
696
+ const flushConsecutiveAwaits = () => {
697
+ if (consecutiveAwaitStatements.length >= SEQUENTIAL_AWAIT_THRESHOLD) reportIfIndependent(consecutiveAwaitStatements, context);
698
+ consecutiveAwaitStatements.length = 0;
699
+ };
700
+ for (const statement of node.body ?? []) if (statement.type === "VariableDeclaration" && statement.declarations?.length === 1 && statement.declarations[0].init?.type === "AwaitExpression" || statement.type === "ExpressionStatement" && statement.expression?.type === "AwaitExpression") consecutiveAwaitStatements.push(statement);
701
+ else flushConsecutiveAwaits();
702
+ flushConsecutiveAwaits();
703
+ } };
704
+ } };
618
705
  const reportIfIndependent = (statements, context) => {
619
706
  const declaredNames = /* @__PURE__ */ new Set();
620
707
  for (const statement of statements) {
@@ -636,12 +723,17 @@ const reportIfIndependent = (statements, context) => {
636
723
 
637
724
  //#endregion
638
725
  //#region src/plugin/rules/nextjs.ts
639
- const nextjsNoImgElement = { create: (context) => ({ JSXOpeningElement(node) {
640
- if (node.name?.type === "JSXIdentifier" && node.name.name === "img") context.report({
641
- node,
642
- message: "Use next/image instead of <img> — provides automatic optimization, lazy loading, and responsive srcset"
643
- });
644
- } }) };
726
+ const nextjsNoImgElement = { create: (context) => {
727
+ const filename = context.getFilename?.() ?? "";
728
+ const isOgRoute = OG_ROUTE_PATTERN.test(filename);
729
+ return { JSXOpeningElement(node) {
730
+ if (isOgRoute) return;
731
+ if (node.name?.type === "JSXIdentifier" && node.name.name === "img") context.report({
732
+ node,
733
+ message: "Use next/image instead of <img> — provides automatic optimization, lazy loading, and responsive srcset"
734
+ });
735
+ } };
736
+ } };
645
737
  const nextjsAsyncClientComponent = { create: (context) => {
646
738
  let fileHasUseClient = false;
647
739
  return {
@@ -706,6 +798,7 @@ const nextjsNoClientFetchForServerData = { create: (context) => {
706
798
  const nextjsMissingMetadata = { create: (context) => ({ Program(programNode) {
707
799
  const filename = context.getFilename?.() ?? "";
708
800
  if (!PAGE_FILE_PATTERN.test(filename)) return;
801
+ if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
709
802
  if (!programNode.body?.some((statement) => {
710
803
  if (statement.type !== "ExportNamedDeclaration") return false;
711
804
  const declaration = statement.declaration;
@@ -873,6 +966,47 @@ const nextjsNoSideEffectInGetHandler = { create: (context) => ({ ExportNamedDecl
873
966
 
874
967
  //#endregion
875
968
  //#region src/plugin/rules/performance.ts
969
+ const isMemoCall = (node) => {
970
+ if (node.type !== "CallExpression") return false;
971
+ if (node.callee?.type === "Identifier" && node.callee.name === "memo") return true;
972
+ if (node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "React" && node.callee.property?.type === "Identifier" && node.callee.property.name === "memo") return true;
973
+ return false;
974
+ };
975
+ const isInlineReference = (node) => {
976
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.property?.name === "bind") return "functions";
977
+ if (node.type === "ObjectExpression") return "objects";
978
+ if (node.type === "ArrayExpression") return "Arrays";
979
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return "JSX";
980
+ return null;
981
+ };
982
+ const noInlinePropOnMemoComponent = { create: (context) => {
983
+ const memoizedComponentNames = /* @__PURE__ */ new Set();
984
+ return {
985
+ VariableDeclarator(node) {
986
+ if (node.id?.type !== "Identifier" || !node.init) return;
987
+ if (isMemoCall(node.init)) memoizedComponentNames.add(node.id.name);
988
+ },
989
+ ExportDefaultDeclaration(node) {
990
+ if (node.declaration && isMemoCall(node.declaration)) {
991
+ const innerArgument = node.declaration.arguments?.[0];
992
+ if (innerArgument?.type === "Identifier") memoizedComponentNames.add(innerArgument.name);
993
+ }
994
+ },
995
+ JSXAttribute(node) {
996
+ if (!node.value || node.value.type !== "JSXExpressionContainer") return;
997
+ const openingElement = node.parent;
998
+ if (!openingElement || openingElement.type !== "JSXOpeningElement") return;
999
+ let elementName = null;
1000
+ if (openingElement.name?.type === "JSXIdentifier") elementName = openingElement.name.name;
1001
+ if (!elementName || !memoizedComponentNames.has(elementName)) return;
1002
+ const propType = isInlineReference(node.value.expression);
1003
+ if (propType) context.report({
1004
+ node: node.value.expression,
1005
+ message: `JSX attribute values should not contain ${propType} created in the same scope — ${elementName} is wrapped in memo(), so new references cause unnecessary re-renders`
1006
+ });
1007
+ }
1008
+ };
1009
+ } };
876
1010
  const noUsememoSimpleExpression = { create: (context) => ({ CallExpression(node) {
877
1011
  if (!isHookCall(node, "useMemo")) return;
878
1012
  const callback = node.arguments?.[0];
@@ -886,9 +1020,18 @@ const noUsememoSimpleExpression = { create: (context) => ({ CallExpression(node)
886
1020
  message: "useMemo wrapping a trivially cheap expression — memo overhead exceeds the computation"
887
1021
  });
888
1022
  } }) };
1023
+ const isMotionElement = (attributeNode) => {
1024
+ const openingElement = attributeNode.parent;
1025
+ if (!openingElement || openingElement.type !== "JSXOpeningElement") return false;
1026
+ const elementName = openingElement.name;
1027
+ if (elementName?.type === "JSXMemberExpression" && elementName.object?.type === "JSXIdentifier" && (elementName.object.name === "motion" || elementName.object.name === "m")) return true;
1028
+ if (elementName?.type === "JSXIdentifier" && elementName.name.startsWith("Motion")) return true;
1029
+ return false;
1030
+ };
889
1031
  const noLayoutPropertyAnimation = { create: (context) => ({ JSXAttribute(node) {
890
1032
  if (node.name?.type !== "JSXIdentifier" || !MOTION_ANIMATE_PROPS.has(node.name.name)) return;
891
1033
  if (!node.value || node.value.type !== "JSXExpressionContainer") return;
1034
+ if (isMotionElement(node)) return;
892
1035
  const expression = node.value.expression;
893
1036
  if (expression?.type !== "ObjectExpression") return;
894
1037
  for (const property of expression.properties ?? []) {
@@ -1019,19 +1162,6 @@ const renderingAnimateSvgWrapper = { create: (context) => ({ JSXOpeningElement(n
1019
1162
  message: "Animation props directly on <svg> — wrap in a <div> or <motion.div> for better rendering performance"
1020
1163
  });
1021
1164
  } }) };
1022
- const renderingUsetransitionLoading = { create: (context) => ({ VariableDeclarator(node) {
1023
- if (node.id?.type !== "ArrayPattern" || !node.id.elements?.length) return;
1024
- if (!node.init || !isHookCall(node.init, "useState")) return;
1025
- if (!node.init.arguments?.length) return;
1026
- const initializer = node.init.arguments[0];
1027
- if (initializer.type !== "Literal" || initializer.value !== false) return;
1028
- const stateVariableName = node.id.elements[0]?.name;
1029
- if (!stateVariableName || !LOADING_STATE_PATTERN.test(stateVariableName)) return;
1030
- context.report({
1031
- node: node.init,
1032
- message: `useState for "${stateVariableName}" — consider useTransition for non-urgent loading states`
1033
- });
1034
- } }) };
1035
1165
  const renderingHydrationNoFlicker = { create: (context) => ({ CallExpression(node) {
1036
1166
  if (!isHookCall(node, EFFECT_HOOK_NAMES) || node.arguments?.length < 2) return;
1037
1167
  const depsNode = node.arguments[1];
@@ -1075,7 +1205,9 @@ const noSecretsInClientCode = { create: (context) => ({ VariableDeclarator(node)
1075
1205
  if (node.init?.type !== "Literal" || typeof node.init.value !== "string") return;
1076
1206
  const variableName = node.id.name;
1077
1207
  const literalValue = node.init.value;
1078
- if (SECRET_VARIABLE_PATTERN.test(variableName) && literalValue.length > SECRET_MIN_LENGTH_CHARS) {
1208
+ const trailingSuffix = variableName.split("_").pop()?.toLowerCase() ?? "";
1209
+ const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
1210
+ if (SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && literalValue.length > SECRET_MIN_LENGTH_CHARS) {
1079
1211
  context.report({
1080
1212
  node,
1081
1213
  message: `Possible hardcoded secret in "${variableName}" — use environment variables instead`
@@ -1121,19 +1253,27 @@ const serverAuthActions = { create: (context) => {
1121
1253
  }
1122
1254
  };
1123
1255
  } };
1124
- const serverAfterNonblocking = { create: (context) => ({ CallExpression(node) {
1125
- if (node.callee?.type !== "MemberExpression") return;
1126
- if (node.callee.property?.type !== "Identifier") return;
1127
- const objectName = node.callee.object?.type === "Identifier" ? node.callee.object.name : null;
1128
- if (!objectName) return;
1129
- const methodName = node.callee.property.name;
1130
- if (!(objectName === "console" && (methodName === "log" || methodName === "info" || methodName === "warn") || objectName === "analytics" && (methodName === "track" || methodName === "identify" || methodName === "page"))) return;
1131
- const filename = context.getFilename?.() ?? "";
1132
- if (SERVER_ACTION_FILE_PATTERN.test(filename) || SERVER_ACTION_DIRECTORY_PATTERN.test(filename)) context.report({
1133
- node,
1134
- message: `${objectName}.${methodName}() in server action use after() for non-blocking logging/analytics`
1135
- });
1136
- } }) };
1256
+ const serverAfterNonblocking = { create: (context) => {
1257
+ let fileHasUseServerDirective = false;
1258
+ return {
1259
+ Program(programNode) {
1260
+ fileHasUseServerDirective = hasDirective(programNode, "use server");
1261
+ },
1262
+ CallExpression(node) {
1263
+ if (!fileHasUseServerDirective) return;
1264
+ if (node.callee?.type !== "MemberExpression") return;
1265
+ if (node.callee.property?.type !== "Identifier") return;
1266
+ const objectName = node.callee.object?.type === "Identifier" ? node.callee.object.name : null;
1267
+ if (!objectName) return;
1268
+ const methodName = node.callee.property.name;
1269
+ if (!(objectName === "console" && (methodName === "log" || methodName === "info" || methodName === "warn") || objectName === "analytics" && (methodName === "track" || methodName === "identify" || methodName === "page"))) return;
1270
+ context.report({
1271
+ node,
1272
+ message: `${objectName}.${methodName}() in server action — use after() for non-blocking logging/analytics`
1273
+ });
1274
+ }
1275
+ };
1276
+ } };
1137
1277
 
1138
1278
  //#endregion
1139
1279
  //#region src/plugin/rules/state-and-effects.ts
@@ -1192,7 +1332,10 @@ const noEffectEventHandler = { create: (context) => ({ CallExpression(node) {
1192
1332
  const depsNode = node.arguments[1];
1193
1333
  if (depsNode.type !== "ArrayExpression" || !depsNode.elements?.length) return;
1194
1334
  const dependencyNames = new Set(depsNode.elements.filter((element) => element?.type === "Identifier").map((element) => element.name));
1195
- if (getCallbackStatements(callback).some((statement) => statement.type === "IfStatement" && statement.test?.type === "Identifier" && dependencyNames.has(statement.test.name))) context.report({
1335
+ const statements = getCallbackStatements(callback);
1336
+ if (statements.length !== 1) return;
1337
+ const soleStatement = statements[0];
1338
+ if (soleStatement.type === "IfStatement" && soleStatement.test?.type === "Identifier" && dependencyNames.has(soleStatement.test.name)) context.report({
1196
1339
  node,
1197
1340
  message: "useEffect simulating an event handler — move logic to an actual event handler instead"
1198
1341
  });
@@ -1214,7 +1357,7 @@ const noDerivedUseState = { create: (context) => {
1214
1357
  if (initializer.type !== "Identifier") return;
1215
1358
  if (componentPropNames.has(initializer.name)) context.report({
1216
1359
  node,
1217
- message: `useState initialized from prop "${initializer.name}" — derive it during render instead of syncing with state`
1360
+ message: `useState initialized from prop "${initializer.name}" — if this value should stay in sync with the prop, derive it during render instead`
1218
1361
  });
1219
1362
  }
1220
1363
  };
@@ -1248,6 +1391,7 @@ const rerenderLazyStateInit = { create: (context) => ({ CallExpression(node) {
1248
1391
  const initializer = node.arguments[0];
1249
1392
  if (initializer.type !== "CallExpression") return;
1250
1393
  const calleeName = initializer.callee?.type === "Identifier" ? initializer.callee.name : initializer.callee?.property?.name ?? "fn";
1394
+ if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
1251
1395
  context.report({
1252
1396
  node: initializer,
1253
1397
  message: `useState(${calleeName}()) calls initializer on every render — use useState(() => ${calleeName}()) for lazy initialization`
@@ -1293,7 +1437,6 @@ const plugin = {
1293
1437
  "rerender-lazy-state-init": rerenderLazyStateInit,
1294
1438
  "rerender-functional-setstate": rerenderFunctionalSetstate,
1295
1439
  "rerender-dependencies": rerenderDependencies,
1296
- "no-generic-handler-names": noGenericHandlerNames,
1297
1440
  "no-giant-component": noGiantComponent,
1298
1441
  "no-render-in-render": noRenderInRender,
1299
1442
  "no-nested-component-definition": noNestedComponentDefinition,
@@ -1301,7 +1444,7 @@ const plugin = {
1301
1444
  "no-layout-property-animation": noLayoutPropertyAnimation,
1302
1445
  "rerender-memo-with-default-value": rerenderMemoWithDefaultValue,
1303
1446
  "rendering-animate-svg-wrapper": renderingAnimateSvgWrapper,
1304
- "rendering-usetransition-loading": renderingUsetransitionLoading,
1447
+ "no-inline-prop-on-memo-component": noInlinePropOnMemoComponent,
1305
1448
  "rendering-hydration-no-flicker": renderingHydrationNoFlicker,
1306
1449
  "no-transition-all": noTransitionAll,
1307
1450
  "no-global-css-variable-animation": noGlobalCssVariableAnimation,