oxlint-plugin-react-doctor 0.7.6-dev.c5a06bd → 0.7.6-dev.dfd47a7
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.js +219 -960
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1180,8 +1180,8 @@ const stripParenExpression = (node) => {
|
|
|
1180
1180
|
return current;
|
|
1181
1181
|
};
|
|
1182
1182
|
//#endregion
|
|
1183
|
-
//#region src/plugin/utils/get-root-identifier.ts
|
|
1184
|
-
const
|
|
1183
|
+
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1184
|
+
const getRootIdentifierName = (node, options) => {
|
|
1185
1185
|
if (!node) return null;
|
|
1186
1186
|
const followCallChains = options?.followCallChains === true;
|
|
1187
1187
|
let cursor = node;
|
|
@@ -1192,18 +1192,16 @@ const getRootIdentifier$1 = (node, options) => {
|
|
|
1192
1192
|
continue;
|
|
1193
1193
|
}
|
|
1194
1194
|
if (followCallChains && isNodeOfType(cursor, "CallExpression")) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1195
|
+
const callee = cursor.callee;
|
|
1196
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
1197
|
+
cursor = callee.object;
|
|
1197
1198
|
continue;
|
|
1198
1199
|
}
|
|
1199
1200
|
break;
|
|
1200
1201
|
}
|
|
1201
|
-
return isNodeOfType(cursor, "Identifier") ? cursor : null;
|
|
1202
|
+
return isNodeOfType(cursor, "Identifier") ? cursor.name : null;
|
|
1202
1203
|
};
|
|
1203
1204
|
//#endregion
|
|
1204
|
-
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1205
|
-
const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
|
|
1206
|
-
//#endregion
|
|
1207
1205
|
//#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
|
|
1208
1206
|
const STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
1209
1207
|
"useCallback",
|
|
@@ -1520,37 +1518,6 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1520
1518
|
}
|
|
1521
1519
|
};
|
|
1522
1520
|
//#endregion
|
|
1523
|
-
//#region src/plugin/utils/is-uppercase-name.ts
|
|
1524
|
-
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
1525
|
-
//#endregion
|
|
1526
|
-
//#region src/plugin/utils/resolve-jsx-element-type.ts
|
|
1527
|
-
const resolveConstantStringBinding = (name) => {
|
|
1528
|
-
const binding = findVariableInitializer(name, name.name);
|
|
1529
|
-
if (!binding?.initializer) return null;
|
|
1530
|
-
const declarator = binding.bindingIdentifier.parent;
|
|
1531
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
1532
|
-
if (declarator.id !== binding.bindingIdentifier) return null;
|
|
1533
|
-
const declaration = declarator.parent;
|
|
1534
|
-
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
1535
|
-
if (declaration.kind !== "const") return null;
|
|
1536
|
-
const initializer = stripParenExpression(binding.initializer);
|
|
1537
|
-
return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
1538
|
-
};
|
|
1539
|
-
const flattenJsxName$2 = (name) => {
|
|
1540
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1541
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1542
|
-
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1543
|
-
return "";
|
|
1544
|
-
};
|
|
1545
|
-
const resolveJsxElementType = (openingElement) => {
|
|
1546
|
-
const name = openingElement.name;
|
|
1547
|
-
if (isNodeOfType(name, "JSXIdentifier")) {
|
|
1548
|
-
if (!isUppercaseName(name.name)) return name.name;
|
|
1549
|
-
return resolveConstantStringBinding(name) ?? name.name;
|
|
1550
|
-
}
|
|
1551
|
-
return flattenJsxName$2(name);
|
|
1552
|
-
};
|
|
1553
|
-
//#endregion
|
|
1554
1521
|
//#region src/plugin/utils/get-element-type.ts
|
|
1555
1522
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1556
1523
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1563,8 +1530,14 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1563
1530
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1564
1531
|
return a11ySettings;
|
|
1565
1532
|
};
|
|
1533
|
+
const flattenJsxName$2 = (name) => {
|
|
1534
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1535
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1536
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1537
|
+
return "";
|
|
1538
|
+
};
|
|
1566
1539
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1567
|
-
const baseName =
|
|
1540
|
+
const baseName = flattenJsxName$2(openingElement.name);
|
|
1568
1541
|
if (a11ySettings.polymorphicPropName) {
|
|
1569
1542
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1570
1543
|
if (polymorphicAttribute) {
|
|
@@ -2399,9 +2372,8 @@ const anchorIsValid = defineRule({
|
|
|
2399
2372
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2400
2373
|
return { JSXOpeningElement(node) {
|
|
2401
2374
|
if (isTestlikeFile) return;
|
|
2402
|
-
|
|
2403
|
-
if (
|
|
2404
|
-
if (tag !== "a") return;
|
|
2375
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2376
|
+
if (getElementType(node, context.settings) !== "a") return;
|
|
2405
2377
|
let hrefAttribute;
|
|
2406
2378
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2407
2379
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5756,7 +5728,7 @@ const buttonHasType = defineRule({
|
|
|
5756
5728
|
return {
|
|
5757
5729
|
JSXOpeningElement(node) {
|
|
5758
5730
|
if (isTestlikeFile) return;
|
|
5759
|
-
if (
|
|
5731
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "button") return;
|
|
5760
5732
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5761
5733
|
if (!typeAttr) {
|
|
5762
5734
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5946,7 +5918,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5946
5918
|
};
|
|
5947
5919
|
return {
|
|
5948
5920
|
JSXOpeningElement(node) {
|
|
5949
|
-
if (
|
|
5921
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
5950
5922
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5951
5923
|
},
|
|
5952
5924
|
CallExpression(node) {
|
|
@@ -5964,7 +5936,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5964
5936
|
//#endregion
|
|
5965
5937
|
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
5966
5938
|
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
5967
|
-
if (!isNodeOfType(node, "Property")
|
|
5939
|
+
if (!isNodeOfType(node, "Property")) return null;
|
|
5968
5940
|
if (node.computed) {
|
|
5969
5941
|
if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
|
|
5970
5942
|
return null;
|
|
@@ -6023,7 +5995,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
6023
5995
|
//#endregion
|
|
6024
5996
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
6025
5997
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
6026
|
-
if (!isNodeOfType(identifier, "Identifier")
|
|
5998
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6027
5999
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6028
6000
|
let symbol = scopes.symbolFor(identifier);
|
|
6029
6001
|
while (symbol?.kind === "const") {
|
|
@@ -6298,131 +6270,6 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6298
6270
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6299
6271
|
};
|
|
6300
6272
|
//#endregion
|
|
6301
|
-
//#region src/plugin/utils/collect-safely-validated-local-storage-keys.ts
|
|
6302
|
-
const isLocalStorageCall = (node, methodName) => {
|
|
6303
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6304
|
-
const callee = stripParenExpression(node.callee);
|
|
6305
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
6306
|
-
const receiver = stripParenExpression(callee.object);
|
|
6307
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "localStorage" && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6308
|
-
};
|
|
6309
|
-
const catchReturnsFallback = (tryStatement) => {
|
|
6310
|
-
const handler = tryStatement.handler;
|
|
6311
|
-
if (!handler) return false;
|
|
6312
|
-
let hasReturn = false;
|
|
6313
|
-
let hasThrow = false;
|
|
6314
|
-
walkAst(handler.body, (child) => {
|
|
6315
|
-
if (isFunctionLike$1(child)) return false;
|
|
6316
|
-
if (isNodeOfType(child, "ReturnStatement")) hasReturn = true;
|
|
6317
|
-
if (isNodeOfType(child, "ThrowStatement")) hasThrow = true;
|
|
6318
|
-
});
|
|
6319
|
-
return hasReturn && !hasThrow;
|
|
6320
|
-
};
|
|
6321
|
-
const resolveValidatorFunction = (callee, scopes) => {
|
|
6322
|
-
const unwrappedCallee = stripParenExpression(callee);
|
|
6323
|
-
if (!isNodeOfType(unwrappedCallee, "Identifier")) return null;
|
|
6324
|
-
const symbol = scopes.symbolFor(unwrappedCallee);
|
|
6325
|
-
if (!symbol) return null;
|
|
6326
|
-
const candidate = symbol.kind === "function" ? symbol.declarationNode : symbol.initializer;
|
|
6327
|
-
return isFunctionLike$1(candidate) ? candidate : null;
|
|
6328
|
-
};
|
|
6329
|
-
const validatorChecksPayloadProperties = (validatorFunction, scopes) => {
|
|
6330
|
-
if (!isFunctionLike$1(validatorFunction)) return false;
|
|
6331
|
-
const firstParameter = validatorFunction.params?.[0];
|
|
6332
|
-
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
6333
|
-
const parameterSymbol = scopes.symbolFor(firstParameter);
|
|
6334
|
-
if (!parameterSymbol) return false;
|
|
6335
|
-
const payloadSymbolIds = new Set([parameterSymbol.id]);
|
|
6336
|
-
let hasPropertyTypeCheck = false;
|
|
6337
|
-
walkAst(validatorFunction.body, (child) => {
|
|
6338
|
-
if (isFunctionLike$1(child)) return false;
|
|
6339
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.init) {
|
|
6340
|
-
const initializer = stripParenExpression(child.init);
|
|
6341
|
-
if (isNodeOfType(initializer, "Identifier")) {
|
|
6342
|
-
const initializerSymbol = scopes.symbolFor(initializer);
|
|
6343
|
-
if (initializerSymbol && payloadSymbolIds.has(initializerSymbol.id)) {
|
|
6344
|
-
const aliasSymbol = scopes.symbolFor(child.id);
|
|
6345
|
-
if (aliasSymbol) payloadSymbolIds.add(aliasSymbol.id);
|
|
6346
|
-
}
|
|
6347
|
-
}
|
|
6348
|
-
}
|
|
6349
|
-
if (!isNodeOfType(child, "UnaryExpression") || child.operator !== "typeof") return;
|
|
6350
|
-
const checkedValue = stripParenExpression(child.argument);
|
|
6351
|
-
if (!isNodeOfType(checkedValue, "MemberExpression")) return;
|
|
6352
|
-
const receiver = stripParenExpression(checkedValue.object);
|
|
6353
|
-
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6354
|
-
const receiverSymbol = scopes.symbolFor(receiver);
|
|
6355
|
-
if (receiverSymbol && payloadSymbolIds.has(receiverSymbol.id)) hasPropertyTypeCheck = true;
|
|
6356
|
-
});
|
|
6357
|
-
return hasPropertyTypeCheck;
|
|
6358
|
-
};
|
|
6359
|
-
const incrementCount = (counts, keyValue) => {
|
|
6360
|
-
counts.set(keyValue, (counts.get(keyValue) ?? 0) + 1);
|
|
6361
|
-
};
|
|
6362
|
-
const isReturnedExpression = (expression) => {
|
|
6363
|
-
const parent = expression.parent;
|
|
6364
|
-
return Boolean(parent && isNodeOfType(parent, "ReturnStatement") && parent.argument === expression);
|
|
6365
|
-
};
|
|
6366
|
-
const collectSafelyValidatedLocalStorageKeys = ({ programNode, scopes, resolveKey }) => {
|
|
6367
|
-
const readCountsByKey = /* @__PURE__ */ new Map();
|
|
6368
|
-
const safeReadCountsByKey = /* @__PURE__ */ new Map();
|
|
6369
|
-
const safeReadSymbolIds = /* @__PURE__ */ new Set();
|
|
6370
|
-
walkAst(programNode, (child) => {
|
|
6371
|
-
if (!isNodeOfType(child, "CallExpression") || !isLocalStorageCall(child, "getItem")) return;
|
|
6372
|
-
const keyArgument = child.arguments?.[0];
|
|
6373
|
-
if (!keyArgument) return;
|
|
6374
|
-
const keyValue = resolveKey(keyArgument);
|
|
6375
|
-
if (keyValue !== null) incrementCount(readCountsByKey, keyValue);
|
|
6376
|
-
});
|
|
6377
|
-
walkAst(programNode, (child) => {
|
|
6378
|
-
if (!isNodeOfType(child, "TryStatement") || !catchReturnsFallback(child)) return;
|
|
6379
|
-
const rawValueKeys = /* @__PURE__ */ new Map();
|
|
6380
|
-
const parsedValueSources = /* @__PURE__ */ new Map();
|
|
6381
|
-
walkAst(child.block, (tryChild) => {
|
|
6382
|
-
if (isFunctionLike$1(tryChild)) return false;
|
|
6383
|
-
if (isNodeOfType(tryChild, "VariableDeclarator") && isNodeOfType(tryChild.id, "Identifier") && tryChild.init) {
|
|
6384
|
-
const initializer = stripParenExpression(tryChild.init);
|
|
6385
|
-
if (isNodeOfType(initializer, "CallExpression") && isLocalStorageCall(initializer, "getItem")) {
|
|
6386
|
-
const keyArgument = initializer.arguments?.[0];
|
|
6387
|
-
const bindingSymbol = scopes.symbolFor(tryChild.id);
|
|
6388
|
-
if (keyArgument && bindingSymbol) {
|
|
6389
|
-
const keyValue = resolveKey(keyArgument);
|
|
6390
|
-
if (keyValue !== null) rawValueKeys.set(bindingSymbol.id, keyValue);
|
|
6391
|
-
}
|
|
6392
|
-
}
|
|
6393
|
-
if (isNodeOfType(initializer, "CallExpression") && isGlobalMethodCall(initializer, "JSON", "parse")) {
|
|
6394
|
-
const rawValueArgument = initializer.arguments?.[0];
|
|
6395
|
-
const parsedValueSymbol = scopes.symbolFor(tryChild.id);
|
|
6396
|
-
if (rawValueArgument && isNodeOfType(rawValueArgument, "Identifier") && parsedValueSymbol) {
|
|
6397
|
-
const rawValueSymbol = scopes.symbolFor(rawValueArgument);
|
|
6398
|
-
if (rawValueSymbol && rawValueKeys.has(rawValueSymbol.id)) parsedValueSources.set(parsedValueSymbol.id, rawValueSymbol.id);
|
|
6399
|
-
}
|
|
6400
|
-
}
|
|
6401
|
-
}
|
|
6402
|
-
if (!isNodeOfType(tryChild, "ConditionalExpression") || !isReturnedExpression(tryChild)) return;
|
|
6403
|
-
const test = stripParenExpression(tryChild.test);
|
|
6404
|
-
if (!isNodeOfType(test, "CallExpression")) return;
|
|
6405
|
-
const testedValue = test.arguments?.[0];
|
|
6406
|
-
if (!testedValue || !isNodeOfType(testedValue, "Identifier")) return;
|
|
6407
|
-
const parsedValueSymbol = scopes.symbolFor(testedValue);
|
|
6408
|
-
if (!parsedValueSymbol) return;
|
|
6409
|
-
const rawValueSymbolId = parsedValueSources.get(parsedValueSymbol.id);
|
|
6410
|
-
if (rawValueSymbolId === void 0) return;
|
|
6411
|
-
const returnedValue = stripParenExpression(tryChild.consequent);
|
|
6412
|
-
if (!isNodeOfType(returnedValue, "Identifier") || scopes.symbolFor(returnedValue)?.id !== parsedValueSymbol.id) return;
|
|
6413
|
-
const validatorFunction = resolveValidatorFunction(test.callee, scopes);
|
|
6414
|
-
if (!validatorFunction || !validatorChecksPayloadProperties(validatorFunction, scopes)) return;
|
|
6415
|
-
const keyValue = rawValueKeys.get(rawValueSymbolId);
|
|
6416
|
-
if (keyValue === void 0 || safeReadSymbolIds.has(rawValueSymbolId)) return;
|
|
6417
|
-
safeReadSymbolIds.add(rawValueSymbolId);
|
|
6418
|
-
incrementCount(safeReadCountsByKey, keyValue);
|
|
6419
|
-
});
|
|
6420
|
-
});
|
|
6421
|
-
const safeKeys = /* @__PURE__ */ new Set();
|
|
6422
|
-
for (const [keyValue, readCount] of readCountsByKey) if (safeReadCountsByKey.get(keyValue) === readCount) safeKeys.add(keyValue);
|
|
6423
|
-
return safeKeys;
|
|
6424
|
-
};
|
|
6425
|
-
//#endregion
|
|
6426
6273
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6427
6274
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6428
6275
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6444,39 +6291,26 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6444
6291
|
severity: "warn",
|
|
6445
6292
|
category: "Correctness",
|
|
6446
6293
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6447
|
-
create: (context) => {
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
if (keyValue === null) return;
|
|
6468
|
-
if (isVersionedKey(keyValue)) return;
|
|
6469
|
-
if (safelyValidatedStorageKeys.has(keyValue)) return;
|
|
6470
|
-
const valueArg = node.arguments?.[1];
|
|
6471
|
-
if (!valueArg) return;
|
|
6472
|
-
if (!isJsonStringifyCall(valueArg)) return;
|
|
6473
|
-
context.report({
|
|
6474
|
-
node: keyArg,
|
|
6475
|
-
message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6476
|
-
});
|
|
6477
|
-
}
|
|
6478
|
-
};
|
|
6479
|
-
}
|
|
6294
|
+
create: (context) => ({ CallExpression(node) {
|
|
6295
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6296
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6297
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6298
|
+
if (receiver.name !== "localStorage") return;
|
|
6299
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6300
|
+
if (node.callee.property.name !== "setItem") return;
|
|
6301
|
+
const keyArg = node.arguments?.[0];
|
|
6302
|
+
if (!keyArg) return;
|
|
6303
|
+
const keyValue = resolveStringKey(keyArg, context);
|
|
6304
|
+
if (keyValue === null) return;
|
|
6305
|
+
if (isVersionedKey(keyValue)) return;
|
|
6306
|
+
const valueArg = node.arguments?.[1];
|
|
6307
|
+
if (!valueArg) return;
|
|
6308
|
+
if (!isJsonStringifyCall(valueArg)) return;
|
|
6309
|
+
context.report({
|
|
6310
|
+
node: keyArg,
|
|
6311
|
+
message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6312
|
+
});
|
|
6313
|
+
} })
|
|
6480
6314
|
});
|
|
6481
6315
|
//#endregion
|
|
6482
6316
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -7599,7 +7433,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7599
7433
|
const getOpeningElementTagName = (openingElement) => {
|
|
7600
7434
|
if (!openingElement) return null;
|
|
7601
7435
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7602
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return
|
|
7436
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement.name.name;
|
|
7603
7437
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7604
7438
|
let cursor = openingElement.name;
|
|
7605
7439
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7851,7 +7685,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7851
7685
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7852
7686
|
return { JSXOpeningElement(node) {
|
|
7853
7687
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7854
|
-
const tagName =
|
|
7688
|
+
const tagName = node.name.name;
|
|
7855
7689
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7856
7690
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7857
7691
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -12083,7 +11917,7 @@ const forbidDomProps = defineRule({
|
|
|
12083
11917
|
return { JSXOpeningElement(node) {
|
|
12084
11918
|
if (forbidMap.size === 0) return;
|
|
12085
11919
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12086
|
-
const elementName =
|
|
11920
|
+
const elementName = node.name.name;
|
|
12087
11921
|
if (isReactComponentName(elementName)) return;
|
|
12088
11922
|
for (const attribute of node.attributes) {
|
|
12089
11923
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -12161,7 +11995,7 @@ const forbidElements = defineRule({
|
|
|
12161
11995
|
return {
|
|
12162
11996
|
JSXOpeningElement(node) {
|
|
12163
11997
|
if (forbidMap.size === 0) return;
|
|
12164
|
-
const fullName =
|
|
11998
|
+
const fullName = flattenJsxName$1(node.name);
|
|
12165
11999
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
12166
12000
|
context.report({
|
|
12167
12001
|
node: node.name,
|
|
@@ -12523,7 +12357,8 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12523
12357
|
const isParagraphElement = (candidate) => {
|
|
12524
12358
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12525
12359
|
const opening = candidate.openingElement;
|
|
12526
|
-
|
|
12360
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12361
|
+
return opening.name.name === "p";
|
|
12527
12362
|
};
|
|
12528
12363
|
const findEnclosingParagraph = (openingElement) => {
|
|
12529
12364
|
const owningElement = openingElement.parent;
|
|
@@ -12544,7 +12379,8 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12544
12379
|
severity: "warn",
|
|
12545
12380
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12546
12381
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12547
|
-
|
|
12382
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12383
|
+
const childTagName = node.name.name;
|
|
12548
12384
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12549
12385
|
if (!findEnclosingParagraph(node)) return;
|
|
12550
12386
|
context.report({
|
|
@@ -12575,7 +12411,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12575
12411
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12576
12412
|
const opening = jsxElement.openingElement;
|
|
12577
12413
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12578
|
-
const tagName =
|
|
12414
|
+
const tagName = opening.name.name;
|
|
12579
12415
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12580
12416
|
return tagName;
|
|
12581
12417
|
};
|
|
@@ -12605,7 +12441,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12605
12441
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12606
12442
|
const opening = ancestor.openingElement;
|
|
12607
12443
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12608
|
-
const ancestorTag =
|
|
12444
|
+
const ancestorTag = opening.name.name;
|
|
12609
12445
|
if (ancestorTag.length === 0) {
|
|
12610
12446
|
previous = ancestor;
|
|
12611
12447
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12687,7 +12523,8 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12687
12523
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12688
12524
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12689
12525
|
const opening = candidate.openingElement;
|
|
12690
|
-
|
|
12526
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12527
|
+
return opening.name.name === tagName;
|
|
12691
12528
|
};
|
|
12692
12529
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12693
12530
|
const owningElement = openingElement.parent;
|
|
@@ -12708,7 +12545,8 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12708
12545
|
severity: "warn",
|
|
12709
12546
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12710
12547
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12711
|
-
|
|
12548
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12549
|
+
const tagName = node.name.name;
|
|
12712
12550
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12713
12551
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12714
12552
|
context.report({
|
|
@@ -12823,7 +12661,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12823
12661
|
matchByOccurrence: true,
|
|
12824
12662
|
create: skipNonProductionFiles((context) => ({
|
|
12825
12663
|
JSXOpeningElement(node) {
|
|
12826
|
-
if (
|
|
12664
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
|
|
12827
12665
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12828
12666
|
if (!sandboxAttr) {
|
|
12829
12667
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -13815,28 +13653,6 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13815
13653
|
} })
|
|
13816
13654
|
});
|
|
13817
13655
|
//#endregion
|
|
13818
|
-
//#region src/plugin/utils/unwrap-discarded-expression.ts
|
|
13819
|
-
const unwrapDiscardedExpression = (node) => {
|
|
13820
|
-
let expression = isNodeOfType(node, "ExpressionStatement") ? node.expression : node;
|
|
13821
|
-
for (;;) {
|
|
13822
|
-
expression = stripParenExpression(expression);
|
|
13823
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") {
|
|
13824
|
-
expression = expression.argument;
|
|
13825
|
-
continue;
|
|
13826
|
-
}
|
|
13827
|
-
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
13828
|
-
const expressions = expression.expressions ?? [];
|
|
13829
|
-
const finalExpression = expressions.at(-1);
|
|
13830
|
-
const prefixExpressions = expressions.slice(0, -1);
|
|
13831
|
-
if (finalExpression && prefixExpressions.length > 0 && prefixExpressions.every((prefixExpression) => isNodeOfType(stripParenExpression(prefixExpression), "Literal"))) {
|
|
13832
|
-
expression = finalExpression;
|
|
13833
|
-
continue;
|
|
13834
|
-
}
|
|
13835
|
-
}
|
|
13836
|
-
return expression;
|
|
13837
|
-
}
|
|
13838
|
-
};
|
|
13839
|
-
//#endregion
|
|
13840
13656
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13841
13657
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13842
13658
|
"forEach",
|
|
@@ -14021,19 +13837,9 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
14021
13837
|
});
|
|
14022
13838
|
return foundAttachment;
|
|
14023
13839
|
};
|
|
14024
|
-
const getStyleAssignment = (node) => {
|
|
14025
|
-
if (!isNodeOfType(node, "ExpressionStatement")) return null;
|
|
14026
|
-
const expression = unwrapDiscardedExpression(node);
|
|
14027
|
-
if (!isNodeOfType(expression, "AssignmentExpression")) return null;
|
|
14028
|
-
if (!isNodeOfType(expression.left, "MemberExpression")) return null;
|
|
14029
|
-
if (!isNodeOfType(expression.left.object, "MemberExpression")) return null;
|
|
14030
|
-
if (!isNodeOfType(expression.left.object.property, "Identifier")) return null;
|
|
14031
|
-
return expression.left.object.property.name === "style" ? expression : null;
|
|
14032
|
-
};
|
|
14033
13840
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
const elementExpression = assignment.left.object.object;
|
|
13841
|
+
if (!isNodeOfType(styleWriteStatement, "ExpressionStatement") || !isNodeOfType(styleWriteStatement.expression, "AssignmentExpression") || !isNodeOfType(styleWriteStatement.expression.left, "MemberExpression") || !isNodeOfType(styleWriteStatement.expression.left.object, "MemberExpression")) return false;
|
|
13842
|
+
const elementExpression = styleWriteStatement.expression.left.object.object;
|
|
14037
13843
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
14038
13844
|
if (!creationRoot) return false;
|
|
14039
13845
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -14045,17 +13851,15 @@ const jsBatchDomCss = defineRule({
|
|
|
14045
13851
|
severity: "warn",
|
|
14046
13852
|
recommendation: "Do all your reads first, then all your writes. Mixing them inside a loop makes the browser recalculate the layout again and again, which is slow",
|
|
14047
13853
|
create: (context) => {
|
|
14048
|
-
const
|
|
14049
|
-
|
|
14050
|
-
return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
|
|
14051
|
-
};
|
|
13854
|
+
const isStyleAssignment = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.object, "MemberExpression") && isNodeOfType(node.expression.left.object.property, "Identifier") && node.expression.left.object.property.name === "style";
|
|
13855
|
+
const writesLayoutAffectingProperty = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(node.expression.left.property.name);
|
|
14052
13856
|
return { BlockStatement(node) {
|
|
14053
13857
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
14054
13858
|
if (!perIterationBody) return;
|
|
14055
13859
|
const statements = node.body ?? [];
|
|
14056
13860
|
let layoutReads = null;
|
|
14057
13861
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
14058
|
-
if (
|
|
13862
|
+
if (!isStyleAssignment(statements[statementIndex]) || !isStyleAssignment(statements[statementIndex - 1])) continue;
|
|
14059
13863
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
14060
13864
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
14061
13865
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -14751,18 +14555,6 @@ const jsHoistRegexp = defineRule({
|
|
|
14751
14555
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14752
14556
|
});
|
|
14753
14557
|
//#endregion
|
|
14754
|
-
//#region src/plugin/utils/resolve-first-argument-binding.ts
|
|
14755
|
-
const resolveFirstArgumentBinding = (firstParameter) => {
|
|
14756
|
-
if (!firstParameter) return null;
|
|
14757
|
-
if (!isNodeOfType(firstParameter, "RestElement")) return firstParameter;
|
|
14758
|
-
if (!isNodeOfType(firstParameter.argument, "ArrayPattern")) return null;
|
|
14759
|
-
const elements = firstParameter.argument.elements ?? [];
|
|
14760
|
-
if (elements.length !== 1) return null;
|
|
14761
|
-
const firstBinding = elements[0];
|
|
14762
|
-
if (!firstBinding || isNodeOfType(firstBinding, "RestElement")) return null;
|
|
14763
|
-
return firstBinding;
|
|
14764
|
-
};
|
|
14765
|
-
//#endregion
|
|
14766
14558
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14767
14559
|
const referencesParameter = (expression, parameterName) => {
|
|
14768
14560
|
if (!expression) return false;
|
|
@@ -14773,7 +14565,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14773
14565
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14774
14566
|
const callback = node.arguments?.[0];
|
|
14775
14567
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14776
|
-
const firstParameter =
|
|
14568
|
+
const firstParameter = callback.params?.[0];
|
|
14777
14569
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14778
14570
|
let predicate = null;
|
|
14779
14571
|
const body = callback.body;
|
|
@@ -15465,21 +15257,9 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15465
15257
|
};
|
|
15466
15258
|
const getResolvedInitializer = (receiver) => {
|
|
15467
15259
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15468
|
-
const
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
|
|
15472
|
-
if (isNodeOfType(initializer, "Identifier")) {
|
|
15473
|
-
const aliased = findVariableInitializer(initializer, initializer.name);
|
|
15474
|
-
if (aliased?.initializer) return {
|
|
15475
|
-
initializer: aliased.initializer,
|
|
15476
|
-
isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
|
|
15477
|
-
};
|
|
15478
|
-
}
|
|
15479
|
-
return {
|
|
15480
|
-
initializer,
|
|
15481
|
-
isDefault
|
|
15482
|
-
};
|
|
15260
|
+
const initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
|
|
15261
|
+
if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
|
|
15262
|
+
return initializer;
|
|
15483
15263
|
};
|
|
15484
15264
|
const isSplitCall = (expression) => {
|
|
15485
15265
|
if (!expression) return false;
|
|
@@ -15645,8 +15425,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15645
15425
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15646
15426
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15647
15427
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15648
|
-
const
|
|
15649
|
-
if (
|
|
15428
|
+
const initializer = getResolvedInitializer(stripped);
|
|
15429
|
+
if (initializer && isSmallInlineLiteralArray(initializer)) return true;
|
|
15650
15430
|
}
|
|
15651
15431
|
return false;
|
|
15652
15432
|
};
|
|
@@ -15698,8 +15478,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15698
15478
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15699
15479
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15700
15480
|
if (resolvedInitializer) {
|
|
15701
|
-
if (isLikelyStringReceiver(resolvedInitializer
|
|
15702
|
-
if (
|
|
15481
|
+
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15482
|
+
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15703
15483
|
}
|
|
15704
15484
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15705
15485
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -16112,21 +15892,14 @@ const jsxFilenameExtension = defineRule({
|
|
|
16112
15892
|
});
|
|
16113
15893
|
//#endregion
|
|
16114
15894
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
16115
|
-
const isJsxFragmentElement = (node
|
|
15895
|
+
const isJsxFragmentElement = (node) => {
|
|
16116
15896
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
16117
15897
|
const elementName = node.name;
|
|
16118
|
-
if (isNodeOfType(elementName, "JSXIdentifier"))
|
|
16119
|
-
if (!scopes) return elementName.name === "Fragment";
|
|
16120
|
-
const symbol = resolveConstIdentifierAlias(elementName, scopes);
|
|
16121
|
-
if (!symbol) return elementName.name === "Fragment" && scopes.isGlobalReference(elementName);
|
|
16122
|
-
return isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "Fragment";
|
|
16123
|
-
}
|
|
15898
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) return elementName.name === "Fragment";
|
|
16124
15899
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
16125
15900
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
16126
|
-
if (elementName.
|
|
16127
|
-
|
|
16128
|
-
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
16129
|
-
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15901
|
+
if (elementName.object.name !== "React") return false;
|
|
15902
|
+
return elementName.property.name === "Fragment";
|
|
16130
15903
|
}
|
|
16131
15904
|
return false;
|
|
16132
15905
|
};
|
|
@@ -16152,7 +15925,7 @@ const jsxFragments = defineRule({
|
|
|
16152
15925
|
if (mode !== "syntax") return;
|
|
16153
15926
|
if (!node.closingElement) return;
|
|
16154
15927
|
const openingElement = node.openingElement;
|
|
16155
|
-
if (!isJsxFragmentElement(openingElement
|
|
15928
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
16156
15929
|
if (openingElement.attributes.length > 0) return;
|
|
16157
15930
|
context.report({
|
|
16158
15931
|
node: openingElement,
|
|
@@ -18833,6 +18606,10 @@ const resolveSettings$29 = (settings) => {
|
|
|
18833
18606
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18834
18607
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18835
18608
|
};
|
|
18609
|
+
const getElementName = (node) => {
|
|
18610
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18611
|
+
return null;
|
|
18612
|
+
};
|
|
18836
18613
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18837
18614
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18838
18615
|
const explicit = options.components?.[elementName];
|
|
@@ -18852,8 +18629,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18852
18629
|
create: (context) => {
|
|
18853
18630
|
const options = resolveSettings$29(context.settings);
|
|
18854
18631
|
return { JSXOpeningElement(node) {
|
|
18855
|
-
|
|
18856
|
-
const elementName = resolveJsxElementType(node);
|
|
18632
|
+
const elementName = getElementName(node);
|
|
18857
18633
|
if (!elementName) return;
|
|
18858
18634
|
for (const attribute of node.attributes) {
|
|
18859
18635
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -19040,6 +18816,11 @@ const checkTarget = (attributeValue) => {
|
|
|
19040
18816
|
alternate: false
|
|
19041
18817
|
};
|
|
19042
18818
|
};
|
|
18819
|
+
const getOpeningElementName = (node) => {
|
|
18820
|
+
const name = node.name;
|
|
18821
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18822
|
+
return null;
|
|
18823
|
+
};
|
|
19043
18824
|
const jsxNoTargetBlank = defineRule({
|
|
19044
18825
|
id: "jsx-no-target-blank",
|
|
19045
18826
|
title: "Unsafe target=_blank link",
|
|
@@ -19059,8 +18840,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
19059
18840
|
return settings.formComponents.has(tagName);
|
|
19060
18841
|
};
|
|
19061
18842
|
return { JSXOpeningElement(node) {
|
|
19062
|
-
|
|
19063
|
-
const tagName = resolveJsxElementType(node);
|
|
18843
|
+
const tagName = getOpeningElementName(node);
|
|
19064
18844
|
if (!tagName) return;
|
|
19065
18845
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
19066
18846
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19293,7 +19073,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19293
19073
|
return {
|
|
19294
19074
|
JSXElement(node) {
|
|
19295
19075
|
const openingElement = node.openingElement;
|
|
19296
|
-
if (!isJsxFragmentElement(openingElement
|
|
19076
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
19297
19077
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19298
19078
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19299
19079
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20535,24 +20315,11 @@ const hasDirective = (programNode, directive) => {
|
|
|
20535
20315
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20536
20316
|
};
|
|
20537
20317
|
//#endregion
|
|
20538
|
-
//#region src/plugin/utils/
|
|
20539
|
-
const
|
|
20540
|
-
|
|
20541
|
-
|
|
20542
|
-
|
|
20543
|
-
]);
|
|
20544
|
-
const OBJECT_FREEZE_OR_SEAL_METHOD_NAMES = new Set(["freeze", "seal"]);
|
|
20545
|
-
const unwrapObjectIntegrityExpression = (node, scopes, methodNames = OBJECT_INTEGRITY_METHOD_NAMES) => {
|
|
20546
|
-
let expression = stripParenExpression(node);
|
|
20547
|
-
while (isNodeOfType(expression, "CallExpression")) {
|
|
20548
|
-
const callee = stripParenExpression(expression.callee);
|
|
20549
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Object" || !scopes.isGlobalReference(callee.object) || !isNodeOfType(callee.property, "Identifier") || !methodNames.has(callee.property.name)) break;
|
|
20550
|
-
const wrappedExpression = expression.arguments[0];
|
|
20551
|
-
if (!wrappedExpression || isNodeOfType(wrappedExpression, "SpreadElement")) break;
|
|
20552
|
-
expression = stripParenExpression(wrappedExpression);
|
|
20553
|
-
}
|
|
20554
|
-
return expression;
|
|
20555
|
-
};
|
|
20318
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20319
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20320
|
+
//#endregion
|
|
20321
|
+
//#region src/plugin/utils/is-component-assignment.ts
|
|
20322
|
+
const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
20556
20323
|
//#endregion
|
|
20557
20324
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20558
20325
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20578,10 +20345,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20578
20345
|
},
|
|
20579
20346
|
VariableDeclarator(node) {
|
|
20580
20347
|
if (!fileHasUseClient) return;
|
|
20348
|
+
if (!isComponentAssignment(node)) return;
|
|
20349
|
+
if (!isInlineFunctionExpression(node.init)) return;
|
|
20350
|
+
if (!node.init.async) return;
|
|
20581
20351
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
20582
|
-
if (!isUppercaseName(node.id.name) || !node.init) return;
|
|
20583
|
-
const componentFunction = unwrapObjectIntegrityExpression(node.init, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES);
|
|
20584
|
-
if (!isInlineFunctionExpression(componentFunction) || !componentFunction.async) return;
|
|
20585
20352
|
context.report({
|
|
20586
20353
|
node,
|
|
20587
20354
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -20883,7 +20650,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20883
20650
|
severity: "warn",
|
|
20884
20651
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20885
20652
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20886
|
-
if (
|
|
20653
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
20887
20654
|
const attributes = node.attributes ?? [];
|
|
20888
20655
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20889
20656
|
if (downloadAttribute) {
|
|
@@ -21079,7 +20846,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
21079
20846
|
severity: "warn",
|
|
21080
20847
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
21081
20848
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21082
|
-
if (
|
|
20849
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21083
20850
|
const attributes = node.attributes ?? [];
|
|
21084
20851
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
21085
20852
|
if (!relAttribute?.value) return;
|
|
@@ -21187,7 +20954,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21187
20954
|
severity: "warn",
|
|
21188
20955
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21189
20956
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21190
|
-
if (
|
|
20957
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21191
20958
|
const attributes = node.attributes ?? [];
|
|
21192
20959
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21193
20960
|
if (!hrefAttribute?.value) return;
|
|
@@ -21362,7 +21129,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21362
21129
|
create: (context) => {
|
|
21363
21130
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21364
21131
|
return { JSXOpeningElement(node) {
|
|
21365
|
-
if (
|
|
21132
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
21366
21133
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21367
21134
|
const programRoot = findProgramRoot(node);
|
|
21368
21135
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21409,8 +21176,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21409
21176
|
severity: "warn",
|
|
21410
21177
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21411
21178
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21412
|
-
|
|
21413
|
-
if (
|
|
21179
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
21180
|
+
if (node.name.name !== "script" && node.name.name !== "Script") return;
|
|
21414
21181
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21415
21182
|
if (!srcAttribute?.value) return;
|
|
21416
21183
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23218,15 +22985,10 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23218
22985
|
if (!declaringNode) return false;
|
|
23219
22986
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23220
22987
|
}));
|
|
23221
|
-
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23222
|
-
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23223
|
-
let declaringFunction = bindingNode.parent;
|
|
23224
|
-
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23225
|
-
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23226
|
-
};
|
|
23227
22988
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23228
22989
|
if (def.type !== "Parameter") return false;
|
|
23229
|
-
|
|
22990
|
+
const bindingParent = def.name.parent;
|
|
22991
|
+
return isFunctionLike$1(bindingParent);
|
|
23230
22992
|
}));
|
|
23231
22993
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23232
22994
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26223,56 +25985,6 @@ const noBarrelImport = defineRule({
|
|
|
26223
25985
|
}
|
|
26224
25986
|
});
|
|
26225
25987
|
//#endregion
|
|
26226
|
-
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26227
|
-
const collectReturnedExpressions = (functionNode) => {
|
|
26228
|
-
if (!isFunctionLike$1(functionNode)) return [];
|
|
26229
|
-
const body = functionNode.body;
|
|
26230
|
-
if (!body) return [];
|
|
26231
|
-
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26232
|
-
const returnedExpressions = [];
|
|
26233
|
-
walkAst(body, (node) => {
|
|
26234
|
-
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26235
|
-
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26236
|
-
});
|
|
26237
|
-
return returnedExpressions;
|
|
26238
|
-
};
|
|
26239
|
-
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26240
|
-
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26241
|
-
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26242
|
-
const functionMatches = (candidateFunction) => {
|
|
26243
|
-
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26244
|
-
visitedFunctions.add(candidateFunction);
|
|
26245
|
-
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26246
|
-
};
|
|
26247
|
-
const expressionMatches = (expression) => {
|
|
26248
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
26249
|
-
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26250
|
-
visitedExpressions.add(unwrappedExpression);
|
|
26251
|
-
if (matchesExpression(unwrappedExpression)) return true;
|
|
26252
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26253
|
-
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26254
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26255
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
26256
|
-
if (isFunctionLike$1(initializer)) return false;
|
|
26257
|
-
return expressionMatches(initializer);
|
|
26258
|
-
}
|
|
26259
|
-
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26260
|
-
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26261
|
-
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26262
|
-
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26263
|
-
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26264
|
-
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26265
|
-
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26266
|
-
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26267
|
-
return functionMatches(candidateFunction);
|
|
26268
|
-
}
|
|
26269
|
-
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26270
|
-
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26271
|
-
return false;
|
|
26272
|
-
};
|
|
26273
|
-
return functionMatches(functionNode);
|
|
26274
|
-
};
|
|
26275
|
-
//#endregion
|
|
26276
25988
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26277
25989
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26278
25990
|
"FunctionDeclaration",
|
|
@@ -26292,13 +26004,12 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26292
26004
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26293
26005
|
};
|
|
26294
26006
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26295
|
-
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
26296
26007
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26297
26008
|
let hasRenderOutput = false;
|
|
26298
26009
|
walkAst(rootNode, (node) => {
|
|
26299
26010
|
if (hasRenderOutput) return false;
|
|
26300
26011
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26301
|
-
if (
|
|
26012
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
|
|
26302
26013
|
hasRenderOutput = true;
|
|
26303
26014
|
return false;
|
|
26304
26015
|
}
|
|
@@ -26309,7 +26020,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26309
26020
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26310
26021
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26311
26022
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26312
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes)
|
|
26023
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26313
26024
|
renderOutputCache.set(functionNode, {
|
|
26314
26025
|
scopes,
|
|
26315
26026
|
hasRenderOutput
|
|
@@ -26317,9 +26028,6 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
26317
26028
|
return hasRenderOutput;
|
|
26318
26029
|
};
|
|
26319
26030
|
//#endregion
|
|
26320
|
-
//#region src/plugin/utils/is-component-assignment.ts
|
|
26321
|
-
const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
26322
|
-
//#endregion
|
|
26323
26031
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
26324
26032
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
26325
26033
|
//#endregion
|
|
@@ -28777,7 +28485,7 @@ const noDisabledZoom = defineRule({
|
|
|
28777
28485
|
category: "Accessibility",
|
|
28778
28486
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28779
28487
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28780
|
-
if (
|
|
28488
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
|
|
28781
28489
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28782
28490
|
if (!nameAttr?.value) return;
|
|
28783
28491
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -29167,7 +28875,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
29167
28875
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
29168
28876
|
for (const statement of componentBody.body ?? []) {
|
|
29169
28877
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
29170
|
-
const expression =
|
|
28878
|
+
const expression = statement.expression;
|
|
29171
28879
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
29172
28880
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
29173
28881
|
effectCalls.push(expression);
|
|
@@ -31949,7 +31657,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31949
31657
|
severity: "warn",
|
|
31950
31658
|
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
31951
31659
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31952
|
-
if (
|
|
31660
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
31953
31661
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31954
31662
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31955
31663
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31961,26 +31669,6 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31961
31669
|
} })
|
|
31962
31670
|
});
|
|
31963
31671
|
//#endregion
|
|
31964
|
-
//#region src/plugin/utils/react-ref-origin.ts
|
|
31965
|
-
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
31966
|
-
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
31967
|
-
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
31968
|
-
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
31969
|
-
if (!symbol?.initializer) return null;
|
|
31970
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
31971
|
-
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
31972
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
31973
|
-
};
|
|
31974
|
-
const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
31975
|
-
const expression = stripParenExpression(node);
|
|
31976
|
-
if (resolveReactRefSymbol(expression, scopes)) return true;
|
|
31977
|
-
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
31978
|
-
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
31979
|
-
if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
31980
|
-
visitedSymbolIds.add(symbol.id);
|
|
31981
|
-
return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
|
|
31982
|
-
};
|
|
31983
|
-
//#endregion
|
|
31984
31672
|
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
31985
31673
|
const TIMER_FUNCTION_NAMES = new Set([
|
|
31986
31674
|
"cancelAnimationFrame",
|
|
@@ -32012,33 +31700,6 @@ const NOTIFICATION_METHOD_NAMES = new Set([
|
|
|
32012
31700
|
"success",
|
|
32013
31701
|
"warning"
|
|
32014
31702
|
]);
|
|
32015
|
-
const NOTIFICATION_MODULE_SOURCES = new Set([
|
|
32016
|
-
"@chakra-ui/react",
|
|
32017
|
-
"@heroui/react",
|
|
32018
|
-
"@mantine/notifications",
|
|
32019
|
-
"antd",
|
|
32020
|
-
"react-hot-toast",
|
|
32021
|
-
"react-toastify",
|
|
32022
|
-
"sonner"
|
|
32023
|
-
]);
|
|
32024
|
-
const SYNCHRONOUS_ARRAY_METHOD_NAMES = new Set([
|
|
32025
|
-
"every",
|
|
32026
|
-
"filter",
|
|
32027
|
-
"find",
|
|
32028
|
-
"findIndex",
|
|
32029
|
-
"flatMap",
|
|
32030
|
-
"forEach",
|
|
32031
|
-
"map",
|
|
32032
|
-
"reduce",
|
|
32033
|
-
"reduceRight",
|
|
32034
|
-
"some",
|
|
32035
|
-
"sort"
|
|
32036
|
-
]);
|
|
32037
|
-
const isNotificationModuleSource = (source) => {
|
|
32038
|
-
if (!source) return false;
|
|
32039
|
-
for (const moduleSource of NOTIFICATION_MODULE_SOURCES) if (source === moduleSource || source.startsWith(`${moduleSource}/`)) return true;
|
|
32040
|
-
return /(?:^|[/_.-])(?:notification|toast)s?(?:$|[/_.-])/i.test(source);
|
|
32041
|
-
};
|
|
32042
31703
|
const getMemberCall = (node) => {
|
|
32043
31704
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
32044
31705
|
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
@@ -32050,37 +31711,27 @@ const getMemberCall = (node) => {
|
|
|
32050
31711
|
};
|
|
32051
31712
|
const isNotificationReceiver = (receiver, scopes) => {
|
|
32052
31713
|
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
32053
|
-
|
|
32054
|
-
if (importBinding) return isNotificationModuleSource(importBinding.source) && (importBinding.exportedName === "default" || importBinding.exportedName !== null && NOTIFICATION_RECEIVER_NAMES.has(importBinding.exportedName));
|
|
31714
|
+
if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
|
|
32055
31715
|
const symbol = scopes.symbolFor(receiver);
|
|
31716
|
+
if (symbol?.kind === "import") return true;
|
|
32056
31717
|
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
32057
31718
|
const callee = symbol.initializer.callee;
|
|
32058
|
-
|
|
32059
|
-
const hookImport = getImportBindingForName(callee, callee.name);
|
|
32060
|
-
return Boolean(hookImport && isNotificationModuleSource(hookImport.source) && hookImport.exportedName !== null && /^use(?:Message|Notification|Toast)$/.test(hookImport.exportedName));
|
|
32061
|
-
};
|
|
32062
|
-
const isKnownGlobalObject = (node, expectedName, scopes) => isNodeOfType(node, "Identifier") && node.name === expectedName && scopes.isGlobalReference(node);
|
|
32063
|
-
const isGlobalMember = (node, expectedPropertyName, scopes) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.property, "Identifier") && node.property.name === expectedPropertyName && (isKnownGlobalObject(node.object, "window", scopes) || isKnownGlobalObject(node.object, "globalThis", scopes));
|
|
32064
|
-
const isStorageReceiver = (node, scopes) => {
|
|
32065
|
-
if (isNodeOfType(node, "Identifier") && STORAGE_RECEIVER_NAMES.has(node.name) && scopes.isGlobalReference(node)) return true;
|
|
32066
|
-
for (const receiverName of STORAGE_RECEIVER_NAMES) if (isGlobalMember(node, receiverName, scopes)) return true;
|
|
32067
|
-
return false;
|
|
31719
|
+
return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
|
|
32068
31720
|
};
|
|
32069
31721
|
const getKnownImpureCall = (callExpression, scopes) => {
|
|
32070
31722
|
if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
|
|
32071
|
-
if (isNodeOfType(callExpression.callee, "MemberExpression") && !callExpression.callee.computed && isNodeOfType(callExpression.callee.property, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.property.name) && (isKnownGlobalObject(callExpression.callee.object, "window", scopes) || isKnownGlobalObject(callExpression.callee.object, "globalThis", scopes))) return `${callExpression.callee.property.name}()`;
|
|
32072
31723
|
const memberCall = getMemberCall(callExpression);
|
|
32073
31724
|
if (!memberCall) return null;
|
|
32074
31725
|
const { methodName, receiver } = memberCall;
|
|
32075
|
-
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) &&
|
|
32076
|
-
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)
|
|
32077
|
-
|
|
32078
|
-
if (EXTERNAL_READ_METHOD_NAMES.has(methodName) && receiverRoot !== null && isKnownGlobalObject(receiverRoot, "document", scopes)) return `.${methodName}()`;
|
|
32079
|
-
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNotificationReceiver(receiver, scopes)) return `${methodName}()`;
|
|
31726
|
+
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
|
|
31727
|
+
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
31728
|
+
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
|
|
32080
31729
|
return null;
|
|
32081
31730
|
};
|
|
32082
31731
|
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
32083
|
-
|
|
31732
|
+
let rootIdentifier = null;
|
|
31733
|
+
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
31734
|
+
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
32084
31735
|
if (!rootIdentifier) return null;
|
|
32085
31736
|
const updaterScope = scopes.ownScopeFor(updater);
|
|
32086
31737
|
if (!updaterScope) return null;
|
|
@@ -32089,26 +31740,12 @@ const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) =>
|
|
|
32089
31740
|
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
32090
31741
|
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
32091
31742
|
};
|
|
32092
|
-
const isArrayValue = (node, scopes) => {
|
|
32093
|
-
const expression = stripParenExpression(node);
|
|
32094
|
-
if (isNodeOfType(expression, "ArrayExpression")) return true;
|
|
32095
|
-
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
32096
|
-
return isNodeOfType(resolveConstIdentifierAlias(expression, scopes)?.initializer, "ArrayExpression");
|
|
32097
|
-
};
|
|
32098
|
-
const isDefinitelySynchronousCallback = (callback, scopes) => {
|
|
32099
|
-
const parent = callback.parent;
|
|
32100
|
-
if (!parent) return false;
|
|
32101
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee === callback) return true;
|
|
32102
|
-
if (!isNodeOfType(parent, "CallExpression") || !parent.arguments?.some((argument) => argument === callback)) return false;
|
|
32103
|
-
if (isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ARRAY_METHOD_NAMES.has(parent.callee.property.name) && isArrayValue(parent.callee.object, scopes)) return true;
|
|
32104
|
-
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.object, "Identifier") && parent.callee.object.name === "Array" && scopes.isGlobalReference(parent.callee.object) && isNodeOfType(parent.callee.property, "Identifier") && parent.callee.property.name === "from" && parent.arguments[1] === callback;
|
|
32105
|
-
};
|
|
32106
31743
|
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
32107
31744
|
const analysis = getProgramAnalysis(updater);
|
|
32108
31745
|
let operation = null;
|
|
32109
31746
|
walkAst(updater, (child) => {
|
|
32110
31747
|
if (operation) return false;
|
|
32111
|
-
if (child !== updater && isFunctionLike$1(child) && !
|
|
31748
|
+
if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
|
|
32112
31749
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32113
31750
|
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
32114
31751
|
const calleeReference = getRef(analysis, child.callee);
|
|
@@ -32140,25 +31777,18 @@ const noImpureStateUpdater = defineRule({
|
|
|
32140
31777
|
severity: "error",
|
|
32141
31778
|
recommendation: "Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.",
|
|
32142
31779
|
create: (context) => ({ CallExpression(node) {
|
|
32143
|
-
const
|
|
32144
|
-
if (!
|
|
31780
|
+
const updater = node.arguments?.[0];
|
|
31781
|
+
if (!updater || !isFunctionLike$1(updater)) return;
|
|
32145
31782
|
const analysis = getProgramAnalysis(node);
|
|
32146
31783
|
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
32147
31784
|
const calleeReference = getRef(analysis, node.callee);
|
|
32148
31785
|
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
32149
31786
|
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
32150
31787
|
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
32151
|
-
let updater = null;
|
|
32152
|
-
if (isFunctionLike$1(updaterArgument)) updater = updaterArgument;
|
|
32153
|
-
else if (isNodeOfType(updaterArgument, "Identifier")) {
|
|
32154
|
-
const updaterReference = getRef(analysis, updaterArgument);
|
|
32155
|
-
if (updaterReference) updater = resolveToFunction(updaterReference);
|
|
32156
|
-
}
|
|
32157
|
-
if (!updater) return;
|
|
32158
31788
|
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
32159
31789
|
if (!operation) return;
|
|
32160
31790
|
context.report({
|
|
32161
|
-
node:
|
|
31791
|
+
node: updater,
|
|
32162
31792
|
message: `This state updater performs ${operation}. React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.`
|
|
32163
31793
|
});
|
|
32164
31794
|
} })
|
|
@@ -32268,7 +31898,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
32268
31898
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
32269
31899
|
return {
|
|
32270
31900
|
JSXOpeningElement(node) {
|
|
32271
|
-
if (
|
|
31901
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
32272
31902
|
let typeAttribute = null;
|
|
32273
31903
|
let typeAttributeIndex = null;
|
|
32274
31904
|
let indeterminateAttribute = null;
|
|
@@ -32447,12 +32077,11 @@ const isMemoCall = (node) => {
|
|
|
32447
32077
|
};
|
|
32448
32078
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
32449
32079
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
32450
|
-
const isInlineReference = (node
|
|
32451
|
-
|
|
32452
|
-
if (isNodeOfType(
|
|
32453
|
-
if (isNodeOfType(
|
|
32454
|
-
if (isNodeOfType(
|
|
32455
|
-
if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
|
|
32080
|
+
const isInlineReference = (node) => {
|
|
32081
|
+
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "bind") return "functions";
|
|
32082
|
+
if (isNodeOfType(node, "ObjectExpression")) return "objects";
|
|
32083
|
+
if (isNodeOfType(node, "ArrayExpression")) return "Arrays";
|
|
32084
|
+
if (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")) return "JSX";
|
|
32456
32085
|
return null;
|
|
32457
32086
|
};
|
|
32458
32087
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -32482,7 +32111,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
32482
32111
|
let elementName = null;
|
|
32483
32112
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
32484
32113
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
32485
|
-
const propType = isInlineReference(node.value.expression
|
|
32114
|
+
const propType = isInlineReference(node.value.expression);
|
|
32486
32115
|
if (propType) context.report({
|
|
32487
32116
|
node: node.value.expression,
|
|
32488
32117
|
message: `This redraws ${elementName} on every render because the prop is ${propType} built right here, so memo() can't skip it. Move it to a stable value with useMemo, useCallback, or module scope`
|
|
@@ -33003,18 +32632,6 @@ const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
|
33003
32632
|
"formatToParts",
|
|
33004
32633
|
"formatRange"
|
|
33005
32634
|
]);
|
|
33006
|
-
const ABSENT_PROPERTY_PROOF = { status: "absent" };
|
|
33007
|
-
const PRESENT_PROPERTY_PROOF = { status: "present" };
|
|
33008
|
-
const UNDEFINED_PROPERTY_PROOF = { status: "undefined" };
|
|
33009
|
-
const UNKNOWN_PROPERTY_PROOF = { status: "unknown" };
|
|
33010
|
-
const READ_ONLY_OBJECT_METHOD_NAMES = new Set([
|
|
33011
|
-
"hasOwnProperty",
|
|
33012
|
-
"isPrototypeOf",
|
|
33013
|
-
"propertyIsEnumerable",
|
|
33014
|
-
"toLocaleString",
|
|
33015
|
-
"toString",
|
|
33016
|
-
"valueOf"
|
|
33017
|
-
]);
|
|
33018
32635
|
const isProvableDateExpression = (expression) => {
|
|
33019
32636
|
if (!expression) return false;
|
|
33020
32637
|
const unwrapped = stripParenExpression(expression);
|
|
@@ -33029,290 +32646,32 @@ const receiverNameLooksDateFlavored = (expression) => {
|
|
|
33029
32646
|
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
33030
32647
|
return false;
|
|
33031
32648
|
};
|
|
33032
|
-
const
|
|
33033
|
-
|
|
33034
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return true;
|
|
33035
|
-
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
33036
|
-
if (expression.name === "undefined" && scopes.isGlobalReference(expression)) return true;
|
|
33037
|
-
const symbol = scopes.symbolFor(expression);
|
|
33038
|
-
if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || visitedSymbolIds.has(symbol.id)) return false;
|
|
33039
|
-
visitedSymbolIds.add(symbol.id);
|
|
33040
|
-
return isStaticUndefined(symbol.initializer, scopes, visitedSymbolIds);
|
|
33041
|
-
};
|
|
33042
|
-
const isNestedAssignmentTarget = (expression) => {
|
|
33043
|
-
let target = expression;
|
|
33044
|
-
let parent = target.parent;
|
|
33045
|
-
while (parent) {
|
|
33046
|
-
if (isNodeOfType(parent, "AssignmentExpression")) return parent.left === target;
|
|
33047
|
-
if (isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement")) return parent.left === target;
|
|
33048
|
-
if (isNodeOfType(parent, "AssignmentPattern")) {
|
|
33049
|
-
if (parent.left !== target) return false;
|
|
33050
|
-
} else if (isNodeOfType(parent, "RestElement")) {
|
|
33051
|
-
if (parent.argument !== target) return false;
|
|
33052
|
-
} else if (isNodeOfType(parent, "ArrayPattern")) {
|
|
33053
|
-
if (!parent.elements?.some((element) => element === target)) return false;
|
|
33054
|
-
} else if (isNodeOfType(parent, "Property")) {
|
|
33055
|
-
if (parent.value !== target || !isNodeOfType(parent.parent, "ObjectPattern")) return false;
|
|
33056
|
-
} else if (isNodeOfType(parent, "ObjectPattern")) {
|
|
33057
|
-
if (!parent.properties?.some((property) => property === target)) return false;
|
|
33058
|
-
} else return false;
|
|
33059
|
-
target = parent;
|
|
33060
|
-
parent = target.parent;
|
|
33061
|
-
}
|
|
33062
|
-
return false;
|
|
33063
|
-
};
|
|
33064
|
-
const isPotentialMutationReference = (identifier, readNode) => {
|
|
33065
|
-
let expression = identifier;
|
|
33066
|
-
let parent = expression.parent;
|
|
33067
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
|
|
33068
|
-
expression = parent;
|
|
33069
|
-
parent = expression.parent;
|
|
33070
|
-
}
|
|
33071
|
-
const rootExpression = expression;
|
|
33072
|
-
let memberDepth = 0;
|
|
33073
|
-
while (parent && isNodeOfType(parent, "MemberExpression") && parent.object === expression) {
|
|
33074
|
-
memberDepth += 1;
|
|
33075
|
-
expression = parent;
|
|
33076
|
-
parent = expression.parent;
|
|
33077
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
|
|
33078
|
-
expression = parent;
|
|
33079
|
-
parent = expression.parent;
|
|
33080
|
-
}
|
|
33081
|
-
}
|
|
33082
|
-
if (!parent || isAstDescendant(identifier, readNode)) return false;
|
|
33083
|
-
if (isNestedAssignmentTarget(expression)) return true;
|
|
33084
|
-
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === expression) return true;
|
|
33085
|
-
if (isNodeOfType(parent, "UpdateExpression") && parent.argument === expression) return true;
|
|
33086
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expression) return true;
|
|
33087
|
-
if (isNodeOfType(parent, "CallExpression")) {
|
|
33088
|
-
if (parent.callee === expression) {
|
|
33089
|
-
if (memberDepth === 0) return true;
|
|
33090
|
-
const memberExpression = stripParenExpression(expression);
|
|
33091
|
-
return memberDepth === 1 && isNodeOfType(memberExpression, "MemberExpression") && !READ_ONLY_OBJECT_METHOD_NAMES.has(getStaticPropertyName(memberExpression) ?? "");
|
|
33092
|
-
}
|
|
33093
|
-
return memberDepth === 0 && parent.arguments?.some((argument) => argument === rootExpression);
|
|
33094
|
-
}
|
|
33095
|
-
if (isNodeOfType(parent, "NewExpression")) return memberDepth === 0 && parent.arguments?.some((argument) => argument === rootExpression);
|
|
33096
|
-
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === rootExpression) return true;
|
|
33097
|
-
return false;
|
|
33098
|
-
};
|
|
33099
|
-
const getSimpleAlias = (identifier, scopes) => {
|
|
33100
|
-
let expression = identifier;
|
|
33101
|
-
let parent = expression.parent;
|
|
33102
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
|
|
33103
|
-
expression = parent;
|
|
33104
|
-
parent = expression.parent;
|
|
33105
|
-
}
|
|
33106
|
-
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== expression || !isNodeOfType(parent.id, "Identifier")) return null;
|
|
33107
|
-
const symbol = scopes.symbolFor(parent.id);
|
|
33108
|
-
return symbol ? {
|
|
33109
|
-
symbol,
|
|
33110
|
-
readNode: parent.id
|
|
33111
|
-
} : null;
|
|
33112
|
-
};
|
|
33113
|
-
const getDirectCallForExpression = (expression) => {
|
|
33114
|
-
let callee = expression;
|
|
33115
|
-
let parent = callee.parent;
|
|
33116
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === callee) {
|
|
33117
|
-
callee = parent;
|
|
33118
|
-
parent = callee.parent;
|
|
33119
|
-
}
|
|
33120
|
-
return isNodeOfType(parent, "CallExpression") && parent.callee === callee ? parent : null;
|
|
33121
|
-
};
|
|
33122
|
-
const getMethodOwner = (functionNode, scopes) => {
|
|
33123
|
-
const methodNode = functionNode.parent;
|
|
33124
|
-
if (isNodeOfType(methodNode, "Property") && methodNode.value === functionNode && isNodeOfType(methodNode.parent, "ObjectExpression")) {
|
|
33125
|
-
const objectParent = methodNode.parent.parent;
|
|
33126
|
-
if (!isNodeOfType(objectParent, "VariableDeclarator") || objectParent.init !== methodNode.parent || !isNodeOfType(objectParent.id, "Identifier")) return null;
|
|
33127
|
-
const ownerSymbol = scopes.symbolFor(objectParent.id);
|
|
33128
|
-
const methodName = getStaticPropertyKeyName(methodNode, { allowComputedString: true });
|
|
33129
|
-
return ownerSymbol && methodName ? {
|
|
33130
|
-
methodName,
|
|
33131
|
-
ownerKind: "object",
|
|
33132
|
-
ownerSymbol,
|
|
33133
|
-
isStatic: false
|
|
33134
|
-
} : null;
|
|
33135
|
-
}
|
|
33136
|
-
if (!isNodeOfType(methodNode, "MethodDefinition") || methodNode.value !== functionNode || !isNodeOfType(methodNode.parent, "ClassBody")) return null;
|
|
33137
|
-
const classNode = methodNode.parent.parent;
|
|
33138
|
-
if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression")) return null;
|
|
33139
|
-
let bindingIdentifier = isNodeOfType(classNode.id, "Identifier") ? classNode.id : null;
|
|
33140
|
-
if (!bindingIdentifier && isNodeOfType(classNode.parent, "VariableDeclarator")) bindingIdentifier = isNodeOfType(classNode.parent.id, "Identifier") ? classNode.parent.id : null;
|
|
33141
|
-
if (!bindingIdentifier) return null;
|
|
33142
|
-
const ownerSymbol = scopes.symbolFor(bindingIdentifier);
|
|
33143
|
-
const methodName = getStaticPropertyKeyName(methodNode, { allowComputedString: true });
|
|
33144
|
-
return ownerSymbol && methodName ? {
|
|
33145
|
-
methodName,
|
|
33146
|
-
ownerKind: "class",
|
|
33147
|
-
ownerSymbol,
|
|
33148
|
-
isStatic: Boolean(methodNode.static)
|
|
33149
|
-
} : null;
|
|
33150
|
-
};
|
|
33151
|
-
const doesSymbolResolveToOwner = (symbol, ownerSymbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
33152
|
-
if (!symbol) return false;
|
|
33153
|
-
if (symbol.declarationNode === ownerSymbol.declarationNode) return true;
|
|
33154
|
-
if (symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
33155
|
-
visitedSymbolIds.add(symbol.id);
|
|
33156
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
33157
|
-
return isNodeOfType(initializer, "Identifier") && doesSymbolResolveToOwner(scopes.symbolFor(initializer), ownerSymbol, scopes, visitedSymbolIds);
|
|
33158
|
-
};
|
|
33159
|
-
const isClassInstanceExpression = (expression, ownerSymbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
33160
|
-
const candidate = stripParenExpression(expression);
|
|
33161
|
-
if (isNodeOfType(candidate, "NewExpression") && isNodeOfType(candidate.callee, "Identifier")) return doesSymbolResolveToOwner(scopes.symbolFor(candidate.callee), ownerSymbol, scopes);
|
|
33162
|
-
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
33163
|
-
const symbol = scopes.symbolFor(candidate);
|
|
33164
|
-
if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
33165
|
-
visitedSymbolIds.add(symbol.id);
|
|
33166
|
-
return isClassInstanceExpression(symbol.initializer, ownerSymbol, scopes, visitedSymbolIds);
|
|
33167
|
-
};
|
|
33168
|
-
const getMethodCalls = (functionNode, scopes) => {
|
|
33169
|
-
const owner = getMethodOwner(functionNode, scopes);
|
|
33170
|
-
if (!owner) return [];
|
|
33171
|
-
const calls = [];
|
|
33172
|
-
walkAst(scopes.rootScope.node, (child) => {
|
|
33173
|
-
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
33174
|
-
if (getStaticPropertyName(child) !== owner.methodName) return;
|
|
33175
|
-
const receiver = stripParenExpression(child.object);
|
|
33176
|
-
let doesReceiverMatch = isNodeOfType(receiver, "Identifier") && doesSymbolResolveToOwner(scopes.symbolFor(receiver), owner.ownerSymbol, scopes);
|
|
33177
|
-
if (owner.ownerKind === "class" && !owner.isStatic) doesReceiverMatch = isClassInstanceExpression(receiver, owner.ownerSymbol, scopes);
|
|
33178
|
-
if (!doesReceiverMatch) return;
|
|
33179
|
-
const call = getDirectCallForExpression(child);
|
|
33180
|
-
if (call) calls.push(call);
|
|
33181
|
-
});
|
|
33182
|
-
return calls;
|
|
33183
|
-
};
|
|
33184
|
-
const isFunctionInvokedBeforeUsage = (functionNode, usageNode, usageBoundary, scopes, visitedSymbolIds, visitedFunctionNodes = /* @__PURE__ */ new Set()) => {
|
|
33185
|
-
if (visitedFunctionNodes.has(functionNode)) return false;
|
|
33186
|
-
visitedFunctionNodes.add(functionNode);
|
|
33187
|
-
const usageFunction = findEnclosingFunction(usageNode);
|
|
33188
|
-
const immediateCall = getDirectCallForExpression(functionNode);
|
|
33189
|
-
if (immediateCall) {
|
|
33190
|
-
const immediateCallFunction = findEnclosingFunction(immediateCall);
|
|
33191
|
-
if (immediateCallFunction === usageFunction) {
|
|
33192
|
-
const immediateCallStart = getRangeStart(immediateCall);
|
|
33193
|
-
return immediateCallStart === null || immediateCallStart < usageBoundary;
|
|
33194
|
-
}
|
|
33195
|
-
if (!immediateCallFunction) return usageFunction !== null;
|
|
33196
|
-
return isFunctionInvokedBeforeUsage(immediateCallFunction, usageNode, usageBoundary, scopes, visitedSymbolIds, new Set(visitedFunctionNodes));
|
|
33197
|
-
}
|
|
33198
|
-
for (const methodCall of getMethodCalls(functionNode, scopes)) {
|
|
33199
|
-
const methodCallFunction = findEnclosingFunction(methodCall);
|
|
33200
|
-
if (methodCallFunction === usageFunction) {
|
|
33201
|
-
const methodCallStart = getRangeStart(methodCall);
|
|
33202
|
-
if (methodCallStart === null || methodCallStart < usageBoundary) return true;
|
|
33203
|
-
continue;
|
|
33204
|
-
}
|
|
33205
|
-
if (!methodCallFunction) {
|
|
33206
|
-
if (usageFunction) return true;
|
|
33207
|
-
continue;
|
|
33208
|
-
}
|
|
33209
|
-
if (isFunctionInvokedBeforeUsage(methodCallFunction, usageNode, usageBoundary, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes))) return true;
|
|
33210
|
-
}
|
|
33211
|
-
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
33212
|
-
if (!bindingIdentifier) return false;
|
|
33213
|
-
const symbol = scopes.symbolFor(bindingIdentifier);
|
|
33214
|
-
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
33215
|
-
visitedSymbolIds.add(symbol.id);
|
|
33216
|
-
let wasInvokedBeforeUsage = false;
|
|
33217
|
-
walkAst(scopes.rootScope.node, (child) => {
|
|
33218
|
-
if (wasInvokedBeforeUsage || !isNodeOfType(child, "Identifier")) return;
|
|
33219
|
-
if (scopes.symbolFor(child)?.declarationNode !== symbol.declarationNode) return;
|
|
33220
|
-
const call = getDirectCallForExpression(child);
|
|
33221
|
-
if (!call) return false;
|
|
33222
|
-
const callFunction = findEnclosingFunction(call);
|
|
33223
|
-
if (callFunction === usageFunction) {
|
|
33224
|
-
const callStart = getRangeStart(call);
|
|
33225
|
-
wasInvokedBeforeUsage = callStart === null || callStart < usageBoundary;
|
|
33226
|
-
return;
|
|
33227
|
-
}
|
|
33228
|
-
if (!callFunction) {
|
|
33229
|
-
wasInvokedBeforeUsage = usageFunction !== null;
|
|
33230
|
-
return;
|
|
33231
|
-
}
|
|
33232
|
-
wasInvokedBeforeUsage = isFunctionInvokedBeforeUsage(callFunction, usageNode, usageBoundary, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes));
|
|
33233
|
-
});
|
|
33234
|
-
return wasInvokedBeforeUsage;
|
|
33235
|
-
};
|
|
33236
|
-
const getMutationUsageBoundary = (usageNode, readNode) => {
|
|
33237
|
-
const readStart = getRangeStart(readNode);
|
|
33238
|
-
let readExpression = readNode;
|
|
33239
|
-
let readParent = readExpression.parent;
|
|
33240
|
-
while (readParent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(readParent.type) && "expression" in readParent && readParent.expression === readExpression) {
|
|
33241
|
-
readExpression = readParent;
|
|
33242
|
-
readParent = readExpression.parent;
|
|
33243
|
-
}
|
|
33244
|
-
if ((isNodeOfType(usageNode, "CallExpression") || isNodeOfType(usageNode, "NewExpression")) && usageNode.arguments?.some((argument) => argument === readExpression)) return usageNode.range?.[1] ?? null;
|
|
33245
|
-
if (isAstDescendant(readNode, usageNode)) return readStart;
|
|
33246
|
-
return getRangeStart(usageNode);
|
|
33247
|
-
};
|
|
33248
|
-
const wasMutatedBeforeUsage = (symbol, usageNode, readNode, scopes, visitedMutationSymbolIds = /* @__PURE__ */ new Set(), inheritedUsageBoundary, readNodesBySymbolId = /* @__PURE__ */ new Map()) => {
|
|
33249
|
-
if (visitedMutationSymbolIds.has(symbol.id)) return false;
|
|
33250
|
-
visitedMutationSymbolIds.add(symbol.id);
|
|
33251
|
-
const usageBoundary = inheritedUsageBoundary === void 0 ? getMutationUsageBoundary(usageNode, readNode) : inheritedUsageBoundary;
|
|
33252
|
-
if (typeof usageBoundary !== "number") return true;
|
|
33253
|
-
const usageFunction = findEnclosingFunction(usageNode);
|
|
33254
|
-
return symbol.references.some((reference) => {
|
|
33255
|
-
const referenceStart = getRangeStart(reference.identifier);
|
|
33256
|
-
const simpleAlias = getSimpleAlias(reference.identifier, scopes);
|
|
33257
|
-
if (simpleAlias) return wasMutatedBeforeUsage(simpleAlias.symbol, usageNode, readNodesBySymbolId.get(simpleAlias.symbol.id) ?? simpleAlias.readNode, scopes, new Set(visitedMutationSymbolIds), usageBoundary, readNodesBySymbolId);
|
|
33258
|
-
if (!isPotentialMutationReference(reference.identifier, readNode)) return false;
|
|
33259
|
-
if (referenceStart === null) return true;
|
|
33260
|
-
const mutationFunction = findEnclosingFunction(reference.identifier);
|
|
33261
|
-
if (mutationFunction === usageFunction) return referenceStart < usageBoundary;
|
|
33262
|
-
if (!mutationFunction) return usageFunction !== null;
|
|
33263
|
-
if (usageFunction && isAstDescendant(usageFunction, mutationFunction)) return true;
|
|
33264
|
-
return isFunctionInvokedBeforeUsage(mutationFunction, usageNode, usageBoundary, scopes, /* @__PURE__ */ new Set());
|
|
33265
|
-
});
|
|
33266
|
-
};
|
|
33267
|
-
const getObjectPropertyProof = (objectExpression, propertyName, scopes, usageNode, visitedSymbolIds = /* @__PURE__ */ new Set(), inheritedUsageBoundary, readNodesBySymbolId = /* @__PURE__ */ new Map()) => {
|
|
33268
|
-
if (!objectExpression) return ABSENT_PROPERTY_PROOF;
|
|
32649
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
32650
|
+
if (!objectExpression) return false;
|
|
33269
32651
|
const unwrapped = stripParenExpression(objectExpression);
|
|
33270
|
-
if (isNodeOfType(unwrapped, "
|
|
33271
|
-
|
|
33272
|
-
|
|
33273
|
-
|
|
33274
|
-
if (
|
|
33275
|
-
|
|
33276
|
-
if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || visitedSymbolIds.has(symbol.id) || wasMutatedBeforeUsage(symbol, usageNode, unwrapped, scopes, /* @__PURE__ */ new Set(), usageBoundary, nextReadNodesBySymbolId)) return UNKNOWN_PROPERTY_PROOF;
|
|
33277
|
-
visitedSymbolIds.add(symbol.id);
|
|
33278
|
-
return getObjectPropertyProof(symbol.initializer, propertyName, scopes, usageNode, visitedSymbolIds, usageBoundary, nextReadNodesBySymbolId);
|
|
33279
|
-
}
|
|
33280
|
-
if (isNodeOfType(unwrapped, "ConditionalExpression")) {
|
|
33281
|
-
const consequent = getObjectPropertyProof(unwrapped.consequent, propertyName, scopes, usageNode, new Set(visitedSymbolIds), inheritedUsageBoundary, new Map(readNodesBySymbolId));
|
|
33282
|
-
const alternate = getObjectPropertyProof(unwrapped.alternate, propertyName, scopes, usageNode, new Set(visitedSymbolIds), inheritedUsageBoundary, new Map(readNodesBySymbolId));
|
|
33283
|
-
return consequent.status === alternate.status ? consequent : UNKNOWN_PROPERTY_PROOF;
|
|
33284
|
-
}
|
|
33285
|
-
if (isNodeOfType(unwrapped, "CallExpression") && isNodeOfType(unwrapped.callee, "MemberExpression") && !unwrapped.callee.computed && isNodeOfType(unwrapped.callee.object, "Identifier") && unwrapped.callee.object.name === "Object" && scopes.isGlobalReference(unwrapped.callee.object) && isNodeOfType(unwrapped.callee.property, "Identifier") && unwrapped.callee.property.name === "freeze") return getObjectPropertyProof(unwrapped.arguments?.[0], propertyName, scopes, usageNode, visitedSymbolIds, inheritedUsageBoundary, readNodesBySymbolId);
|
|
33286
|
-
if (!isNodeOfType(unwrapped, "ObjectExpression")) return UNKNOWN_PROPERTY_PROOF;
|
|
33287
|
-
const properties = unwrapped.properties ?? [];
|
|
33288
|
-
for (let propertyIndex = properties.length - 1; propertyIndex >= 0; propertyIndex -= 1) {
|
|
33289
|
-
const property = properties[propertyIndex];
|
|
33290
|
-
if (!property) continue;
|
|
33291
|
-
if (isNodeOfType(property, "SpreadElement")) {
|
|
33292
|
-
const spreadProof = getObjectPropertyProof(property.argument, propertyName, scopes, unwrapped, new Set(visitedSymbolIds), void 0);
|
|
33293
|
-
if (spreadProof.status !== "absent") return spreadProof;
|
|
33294
|
-
continue;
|
|
33295
|
-
}
|
|
33296
|
-
if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== propertyName) continue;
|
|
33297
|
-
return isStaticUndefined(property.value, scopes) ? UNDEFINED_PROPERTY_PROOF : PRESENT_PROPERTY_PROOF;
|
|
32652
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
32653
|
+
for (const property of unwrapped.properties ?? []) {
|
|
32654
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
32655
|
+
if (property.computed) continue;
|
|
32656
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
32657
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
33298
32658
|
}
|
|
33299
|
-
return
|
|
32659
|
+
return false;
|
|
33300
32660
|
};
|
|
33301
|
-
const
|
|
33302
|
-
const hasExplicitLocaleArgument = (argument, scopes) => {
|
|
32661
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
33303
32662
|
if (!argument) return false;
|
|
33304
32663
|
const unwrapped = stripParenExpression(argument);
|
|
33305
|
-
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined"
|
|
32664
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
33306
32665
|
return true;
|
|
33307
32666
|
};
|
|
33308
|
-
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate
|
|
32667
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
33309
32668
|
const localeArgument = call.arguments?.[0];
|
|
33310
|
-
if (!hasExplicitLocaleArgument(localeArgument
|
|
32669
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
33311
32670
|
const optionsArgument = call.arguments?.[1];
|
|
33312
|
-
if (
|
|
32671
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
33313
32672
|
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
33314
32673
|
};
|
|
33315
|
-
const matchLocaleMethodCall = (call
|
|
32674
|
+
const matchLocaleMethodCall = (call) => {
|
|
33316
32675
|
const callee = call.callee;
|
|
33317
32676
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
33318
32677
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
@@ -33320,7 +32679,7 @@ const matchLocaleMethodCall = (call, scopes) => {
|
|
|
33320
32679
|
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
33321
32680
|
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
33322
32681
|
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
33323
|
-
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate
|
|
32682
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
33324
32683
|
return {
|
|
33325
32684
|
node: call,
|
|
33326
32685
|
display: `${methodName}()`
|
|
@@ -33336,13 +32695,13 @@ const getIntlFormatterName = (expression) => {
|
|
|
33336
32695
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
33337
32696
|
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
33338
32697
|
};
|
|
33339
|
-
const isDeterministicIntlConstruction = (construction, formatterName
|
|
32698
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
33340
32699
|
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
33341
|
-
if (!hasExplicitLocaleArgument(construction.arguments?.[0]
|
|
32700
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
33342
32701
|
if (formatterName !== "DateTimeFormat") return true;
|
|
33343
|
-
return
|
|
32702
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
33344
32703
|
};
|
|
33345
|
-
const matchIntlFormatCall = (call
|
|
32704
|
+
const matchIntlFormatCall = (call) => {
|
|
33346
32705
|
const callee = call.callee;
|
|
33347
32706
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
33348
32707
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
@@ -33355,7 +32714,7 @@ const matchIntlFormatCall = (call, scopes) => {
|
|
|
33355
32714
|
if (!construction) return null;
|
|
33356
32715
|
const formatterName = getIntlFormatterName(construction);
|
|
33357
32716
|
if (!formatterName) return null;
|
|
33358
|
-
if (isDeterministicIntlConstruction(construction, formatterName
|
|
32717
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
33359
32718
|
return {
|
|
33360
32719
|
node: call,
|
|
33361
32720
|
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
@@ -33424,7 +32783,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
33424
32783
|
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
33425
32784
|
},
|
|
33426
32785
|
CallExpression(node) {
|
|
33427
|
-
const match = matchLocaleMethodCall(node
|
|
32786
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
33428
32787
|
if (match) reportIfRenderPhase(match);
|
|
33429
32788
|
},
|
|
33430
32789
|
TemplateLiteral(node) {
|
|
@@ -33444,7 +32803,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
33444
32803
|
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
33445
32804
|
if (isFunctionLike$1(child)) return false;
|
|
33446
32805
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
33447
|
-
const match = matchLocaleMethodCall(child
|
|
32806
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
33448
32807
|
if (!match || reportedNodes.has(match.node)) return;
|
|
33449
32808
|
if (fileIsEmailTemplate) return;
|
|
33450
32809
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
@@ -33611,13 +32970,11 @@ const noManyBooleanProps = defineRule({
|
|
|
33611
32970
|
};
|
|
33612
32971
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33613
32972
|
if (!param) return;
|
|
33614
|
-
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33615
|
-
if (!propsBinding) return;
|
|
33616
32973
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33617
|
-
if (isNodeOfType(
|
|
32974
|
+
if (isNodeOfType(param, "ObjectPattern")) {
|
|
33618
32975
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33619
32976
|
const booleanLikePropNames = [];
|
|
33620
|
-
for (const property of
|
|
32977
|
+
for (const property of param.properties ?? []) {
|
|
33621
32978
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33622
32979
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33623
32980
|
if (!keyName) continue;
|
|
@@ -33628,7 +32985,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33628
32985
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33629
32986
|
return;
|
|
33630
32987
|
}
|
|
33631
|
-
if (isNodeOfType(
|
|
32988
|
+
if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
|
|
33632
32989
|
};
|
|
33633
32990
|
return {
|
|
33634
32991
|
FunctionDeclaration(node) {
|
|
@@ -33750,7 +33107,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33750
33107
|
if (mirrorBindings.length === 0) return;
|
|
33751
33108
|
for (const statement of componentBody.body ?? []) {
|
|
33752
33109
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
33753
|
-
const effectCall =
|
|
33110
|
+
const effectCall = statement.expression;
|
|
33754
33111
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
33755
33112
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
33756
33113
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -33764,7 +33121,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33764
33121
|
const bodyStatements = getCallbackStatements(callback);
|
|
33765
33122
|
if (bodyStatements.length !== 1) continue;
|
|
33766
33123
|
const onlyStatement = bodyStatements[0];
|
|
33767
|
-
const expression =
|
|
33124
|
+
const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
|
|
33768
33125
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
33769
33126
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
33770
33127
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -36380,7 +35737,7 @@ const noPreventDefault = defineRule({
|
|
|
36380
35737
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
36381
35738
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
36382
35739
|
return { JSXOpeningElement(node) {
|
|
36383
|
-
const elementName =
|
|
35740
|
+
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
36384
35741
|
if (!elementName) return;
|
|
36385
35742
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
36386
35743
|
if (!targetEventProps) return;
|
|
@@ -37270,85 +36627,28 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
37270
36627
|
});
|
|
37271
36628
|
//#endregion
|
|
37272
36629
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
37273
|
-
const
|
|
37274
|
-
"
|
|
37275
|
-
"
|
|
37276
|
-
|
|
37277
|
-
|
|
37278
|
-
|
|
37279
|
-
|
|
36630
|
+
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
36631
|
+
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
36632
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
36633
|
+
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
36634
|
+
if (!symbol?.initializer) return null;
|
|
36635
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
36636
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
36637
|
+
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
36638
|
+
};
|
|
37280
36639
|
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
37281
|
-
if (!isNodeOfType(node, "MemberExpression") ||
|
|
36640
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
37282
36641
|
const receiver = stripParenExpression(node.object);
|
|
37283
36642
|
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
37284
36643
|
};
|
|
37285
|
-
const isSameRefCurrentAlias = (node, refSymbol, scopes) => {
|
|
37286
|
-
if (isSameRefCurrentMember(node, refSymbol, scopes)) return true;
|
|
37287
|
-
if (!isNodeOfType(node, "Identifier")) return false;
|
|
37288
|
-
const aliasSymbol = scopes.symbolFor(node);
|
|
37289
|
-
return aliasSymbol?.kind === "const" && aliasSymbol.initializer !== null && isSameRefCurrentMember(stripParenExpression(aliasSymbol.initializer), refSymbol, scopes);
|
|
37290
|
-
};
|
|
37291
|
-
const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
|
|
37292
|
-
const hasRepeatedExecutionAncestor = (node, stop) => {
|
|
37293
|
-
let ancestor = node.parent;
|
|
37294
|
-
while (ancestor && ancestor !== stop) {
|
|
37295
|
-
if (isFunctionLike$1(ancestor) || REPEATED_ANCESTOR_TYPES.has(ancestor.type)) return true;
|
|
37296
|
-
ancestor = ancestor.parent;
|
|
37297
|
-
}
|
|
37298
|
-
return ancestor !== stop;
|
|
37299
|
-
};
|
|
37300
|
-
const getBranchConstraints = (node, branchRoot) => {
|
|
37301
|
-
const constraints = /* @__PURE__ */ new Map();
|
|
37302
|
-
let descendant = node;
|
|
37303
|
-
let ancestor = descendant.parent;
|
|
37304
|
-
while (ancestor && descendant !== branchRoot) {
|
|
37305
|
-
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
37306
|
-
if (ancestor.consequent === descendant) constraints.set(ancestor, true);
|
|
37307
|
-
if (ancestor.alternate === descendant) constraints.set(ancestor, false);
|
|
37308
|
-
}
|
|
37309
|
-
descendant = ancestor;
|
|
37310
|
-
ancestor = ancestor.parent;
|
|
37311
|
-
}
|
|
37312
|
-
return constraints;
|
|
37313
|
-
};
|
|
37314
|
-
const canExecuteTogether = (firstConstraints, secondConstraints) => {
|
|
37315
|
-
for (const [statement, branch] of firstConstraints) {
|
|
37316
|
-
const otherBranch = secondConstraints.get(statement);
|
|
37317
|
-
if (otherBranch !== void 0 && otherBranch !== branch) return false;
|
|
37318
|
-
}
|
|
37319
|
-
return true;
|
|
37320
|
-
};
|
|
37321
|
-
const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol, scopes) => {
|
|
37322
|
-
const assignmentConstraints = getBranchConstraints(assignmentExpression, branchRoot);
|
|
37323
|
-
const assignmentStart = getRangeStart(assignmentExpression);
|
|
37324
|
-
let hasCoExecutableWrite = false;
|
|
37325
|
-
walkAst(branchRoot, (child) => {
|
|
37326
|
-
if (hasCoExecutableWrite) return false;
|
|
37327
|
-
const childStart = getRangeStart(child);
|
|
37328
|
-
if (child === assignmentExpression || !isNodeOfType(child, "AssignmentExpression") || assignmentStart === null || childStart === null || childStart >= assignmentStart || resolveReactRefSymbol(child.left, scopes)?.id !== refSymbol.id || hasRepeatedExecutionAncestor(child, branchRoot)) return;
|
|
37329
|
-
if (canExecuteTogether(assignmentConstraints, getBranchConstraints(child, branchRoot))) {
|
|
37330
|
-
hasCoExecutableWrite = true;
|
|
37331
|
-
return false;
|
|
37332
|
-
}
|
|
37333
|
-
});
|
|
37334
|
-
return !hasCoExecutableWrite;
|
|
37335
|
-
};
|
|
37336
36644
|
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
37337
|
-
if (assignmentExpression.operator
|
|
37338
|
-
if (assignmentExpression.operator !== "=") return false;
|
|
36645
|
+
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
37339
36646
|
let descendant = assignmentExpression;
|
|
37340
36647
|
let ancestor = descendant.parent;
|
|
37341
36648
|
while (ancestor) {
|
|
37342
|
-
if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(ancestor.test, "BinaryExpression") &&
|
|
37343
|
-
"===",
|
|
37344
|
-
"==",
|
|
37345
|
-
"!==",
|
|
37346
|
-
"!="
|
|
37347
|
-
].includes(ancestor.test.operator)) {
|
|
36649
|
+
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
37348
36650
|
const { left, right } = ancestor.test;
|
|
37349
|
-
|
|
37350
|
-
const guardedBranch = ancestor.test.operator === "===" || ancestor.test.operator === "==" ? ancestor.consequent : ancestor.alternate;
|
|
37351
|
-
if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && hasNoPriorCoExecutableWrite(assignmentExpression, guardedBranch, refSymbol, scopes)) return true;
|
|
36651
|
+
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
37352
36652
|
}
|
|
37353
36653
|
descendant = ancestor;
|
|
37354
36654
|
ancestor = descendant.parent;
|
|
@@ -37922,7 +37222,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37922
37222
|
return expressionReadsStateValue(argument, stateName);
|
|
37923
37223
|
};
|
|
37924
37224
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37925
|
-
const expression =
|
|
37225
|
+
const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
|
|
37926
37226
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37927
37227
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37928
37228
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -38271,7 +37571,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
38271
37571
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
38272
37572
|
for (const statement of functionBody.body ?? []) {
|
|
38273
37573
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
38274
|
-
const effectCall =
|
|
37574
|
+
const effectCall = statement.expression;
|
|
38275
37575
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
38276
37576
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
38277
37577
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -38901,10 +38201,9 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38901
38201
|
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
38902
38202
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38903
38203
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38904
|
-
const
|
|
38905
|
-
const firstCharacter = elementName.charCodeAt(0);
|
|
38204
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
38906
38205
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38907
|
-
if (
|
|
38206
|
+
if (node.name.name.includes("-")) return;
|
|
38908
38207
|
for (const attribute of node.attributes) {
|
|
38909
38208
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38910
38209
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -39292,7 +38591,8 @@ const noUncontrolledInput = defineRule({
|
|
|
39292
38591
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
39293
38592
|
walkAst(componentBody, (child) => {
|
|
39294
38593
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
39295
|
-
|
|
38594
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
38595
|
+
const tagName = child.name.name;
|
|
39296
38596
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
39297
38597
|
const attributes = child.attributes ?? [];
|
|
39298
38598
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -39353,7 +38653,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
39353
38653
|
severity: "warn",
|
|
39354
38654
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
39355
38655
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
39356
|
-
if (
|
|
38656
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
|
|
39357
38657
|
const attributes = node.attributes ?? [];
|
|
39358
38658
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
39359
38659
|
if (!srcAttribute) return;
|
|
@@ -40568,7 +39868,7 @@ const noUnknownProperty = defineRule({
|
|
|
40568
39868
|
}
|
|
40569
39869
|
if (fileIsNonReactJsx) return;
|
|
40570
39870
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
40571
|
-
const elementType =
|
|
39871
|
+
const elementType = node.name.name;
|
|
40572
39872
|
const firstCharacter = elementType.charCodeAt(0);
|
|
40573
39873
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
40574
39874
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40746,21 +40046,20 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40746
40046
|
});
|
|
40747
40047
|
return found;
|
|
40748
40048
|
};
|
|
40749
|
-
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40750
40049
|
const isReactClassComponent = (classNode) => {
|
|
40751
40050
|
if (isEs6Component(classNode)) return true;
|
|
40752
40051
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40753
40052
|
};
|
|
40754
|
-
const findEnclosingComponent = (node
|
|
40053
|
+
const findEnclosingComponent = (node) => {
|
|
40755
40054
|
let walker = node.parent;
|
|
40756
40055
|
while (walker) {
|
|
40757
40056
|
if (isFunctionLike$1(walker)) {
|
|
40758
40057
|
const componentName = inferFunctionLikeName(walker);
|
|
40759
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40058
|
+
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
40760
40059
|
component: walker,
|
|
40761
40060
|
name: componentName
|
|
40762
40061
|
};
|
|
40763
|
-
if (!componentName &&
|
|
40062
|
+
if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40764
40063
|
component: walker,
|
|
40765
40064
|
name: null
|
|
40766
40065
|
};
|
|
@@ -40999,7 +40298,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40999
40298
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
41000
40299
|
if (settings.allowAsProps) return;
|
|
41001
40300
|
}
|
|
41002
|
-
const enclosing = findEnclosingComponent(candidateNode
|
|
40301
|
+
const enclosing = findEnclosingComponent(candidateNode);
|
|
41003
40302
|
if (!enclosing) return;
|
|
41004
40303
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
41005
40304
|
queuedReports.push({
|
|
@@ -41010,7 +40309,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
41010
40309
|
});
|
|
41011
40310
|
};
|
|
41012
40311
|
const checkFunctionLike = (node) => {
|
|
41013
|
-
if (!
|
|
40312
|
+
if (!expressionContainsJsxOrCreateElement(node)) return;
|
|
41014
40313
|
const inferredName = inferFunctionLikeName(node);
|
|
41015
40314
|
const propInfo = isComponentDeclaredInProp(node);
|
|
41016
40315
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -42270,7 +41569,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
42270
41569
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
42271
41570
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
42272
41571
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42273
|
-
const tagName =
|
|
41572
|
+
const tagName = node.name.name;
|
|
42274
41573
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42275
41574
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
42276
41575
|
if (!onDoubleClickAttribute) return;
|
|
@@ -42290,7 +41589,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
42290
41589
|
]);
|
|
42291
41590
|
const isTextLikeInput = (openingElement) => {
|
|
42292
41591
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
42293
|
-
const tagName =
|
|
41592
|
+
const tagName = openingElement.name.name;
|
|
42294
41593
|
if (tagName === "textarea") return true;
|
|
42295
41594
|
if (tagName !== "input") return false;
|
|
42296
41595
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -42447,9 +41746,8 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
42447
41746
|
]);
|
|
42448
41747
|
const collectBooleanPropBindings = (param) => {
|
|
42449
41748
|
const bindings = /* @__PURE__ */ new Set();
|
|
42450
|
-
|
|
42451
|
-
|
|
42452
|
-
for (const property of propsBinding.properties ?? []) {
|
|
41749
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
41750
|
+
for (const property of param.properties ?? []) {
|
|
42453
41751
|
if (!isNodeOfType(property, "Property")) continue;
|
|
42454
41752
|
if (property.computed) continue;
|
|
42455
41753
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42710,7 +42008,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42710
42008
|
},
|
|
42711
42009
|
JSXOpeningElement(node) {
|
|
42712
42010
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42713
|
-
const tagName =
|
|
42011
|
+
const tagName = node.name.name;
|
|
42714
42012
|
if (tagName === "dialog") return;
|
|
42715
42013
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42716
42014
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -44257,13 +43555,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
44257
43555
|
}
|
|
44258
43556
|
return null;
|
|
44259
43557
|
};
|
|
44260
|
-
const resolveHookNameFromInitializer = (initializer
|
|
43558
|
+
const resolveHookNameFromInitializer = (initializer) => {
|
|
44261
43559
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
44262
43560
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
44263
|
-
const
|
|
44264
|
-
if (
|
|
44265
|
-
if (!isNodeOfType(
|
|
44266
|
-
return resolveTanstackQueryHookName(
|
|
43561
|
+
const binding = findVariableInitializer(initializer, initializer.name);
|
|
43562
|
+
if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
|
|
43563
|
+
if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
43564
|
+
return resolveTanstackQueryHookName(binding.initializer);
|
|
44267
43565
|
};
|
|
44268
43566
|
const queryNoRestDestructuring = defineRule({
|
|
44269
43567
|
id: "query-no-rest-destructuring",
|
|
@@ -44276,7 +43574,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
44276
43574
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
44277
43575
|
if (!node.init) return;
|
|
44278
43576
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
44279
|
-
const hookName = resolveHookNameFromInitializer(node.init
|
|
43577
|
+
const hookName = resolveHookNameFromInitializer(node.init);
|
|
44280
43578
|
if (!hookName) return;
|
|
44281
43579
|
context.report({
|
|
44282
43580
|
node: node.id,
|
|
@@ -44747,7 +44045,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44747
44045
|
severity: "warn",
|
|
44748
44046
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44749
44047
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44750
|
-
if (
|
|
44048
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
44751
44049
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44752
44050
|
node,
|
|
44753
44051
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45631,7 +44929,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
|
|
|
45631
44929
|
"useParams",
|
|
45632
44930
|
"usePathname"
|
|
45633
44931
|
]);
|
|
45634
|
-
const findHookCallBindings = (componentBody
|
|
44932
|
+
const findHookCallBindings = (componentBody) => {
|
|
45635
44933
|
const bindings = [];
|
|
45636
44934
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
45637
44935
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -45642,10 +44940,8 @@ const findHookCallBindings = (componentBody, scopes) => {
|
|
|
45642
44940
|
const callee = declarator.init.callee;
|
|
45643
44941
|
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
45644
44942
|
if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
|
|
45645
|
-
const valueSymbol = scopes.symbolFor(declarator.id);
|
|
45646
|
-
if (!valueSymbol) continue;
|
|
45647
44943
|
bindings.push({
|
|
45648
|
-
|
|
44944
|
+
valueName: declarator.id.name,
|
|
45649
44945
|
hookName: callee.name,
|
|
45650
44946
|
declarator
|
|
45651
44947
|
});
|
|
@@ -45653,49 +44949,6 @@ const findHookCallBindings = (componentBody, scopes) => {
|
|
|
45653
44949
|
}
|
|
45654
44950
|
return bindings;
|
|
45655
44951
|
};
|
|
45656
|
-
const collectExactAliasSymbols = (componentBody, sourceSymbol, scopes) => {
|
|
45657
|
-
const symbols = [sourceSymbol];
|
|
45658
|
-
const symbolIds = new Set([sourceSymbol.id]);
|
|
45659
|
-
const aliasSourceIdentifiers = /* @__PURE__ */ new Set();
|
|
45660
|
-
if (!isNodeOfType(componentBody, "BlockStatement")) return {
|
|
45661
|
-
symbols,
|
|
45662
|
-
aliasSourceIdentifiers
|
|
45663
|
-
};
|
|
45664
|
-
let didFindAlias = true;
|
|
45665
|
-
while (didFindAlias) {
|
|
45666
|
-
didFindAlias = false;
|
|
45667
|
-
for (const statement of componentBody.body ?? []) {
|
|
45668
|
-
if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "const") continue;
|
|
45669
|
-
for (const declarator of statement.declarations ?? []) {
|
|
45670
|
-
let aliasIdentifier = null;
|
|
45671
|
-
let sourceIdentifier = null;
|
|
45672
|
-
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
45673
|
-
const arrayBinding = isNodeOfType(declarator.id, "ArrayPattern") ? declarator.id.elements[0] : null;
|
|
45674
|
-
const arrayValueNode = isNodeOfType(initializer, "ArrayExpression") ? initializer.elements[0] : null;
|
|
45675
|
-
const arrayValue = arrayValueNode && !isNodeOfType(arrayValueNode, "SpreadElement") ? stripParenExpression(arrayValueNode) : null;
|
|
45676
|
-
if (isNodeOfType(declarator.id, "Identifier") && isNodeOfType(initializer, "Identifier")) {
|
|
45677
|
-
aliasIdentifier = declarator.id;
|
|
45678
|
-
sourceIdentifier = initializer;
|
|
45679
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements.length === 1 && isNodeOfType(arrayBinding, "Identifier") && isNodeOfType(initializer, "ArrayExpression") && initializer.elements.length === 1 && isNodeOfType(arrayValue, "Identifier")) {
|
|
45680
|
-
aliasIdentifier = arrayBinding;
|
|
45681
|
-
sourceIdentifier = arrayValue;
|
|
45682
|
-
}
|
|
45683
|
-
if (!aliasIdentifier || !sourceIdentifier) continue;
|
|
45684
|
-
const referencedSymbol = scopes.symbolFor(sourceIdentifier);
|
|
45685
|
-
const aliasSymbol = scopes.symbolFor(aliasIdentifier);
|
|
45686
|
-
if (!referencedSymbol || !symbolIds.has(referencedSymbol.id) || !aliasSymbol || aliasSymbol.kind !== "const" || symbolIds.has(aliasSymbol.id)) continue;
|
|
45687
|
-
symbolIds.add(aliasSymbol.id);
|
|
45688
|
-
symbols.push(aliasSymbol);
|
|
45689
|
-
aliasSourceIdentifiers.add(sourceIdentifier);
|
|
45690
|
-
didFindAlias = true;
|
|
45691
|
-
}
|
|
45692
|
-
}
|
|
45693
|
-
}
|
|
45694
|
-
return {
|
|
45695
|
-
symbols,
|
|
45696
|
-
aliasSourceIdentifiers
|
|
45697
|
-
};
|
|
45698
|
-
};
|
|
45699
44952
|
const rerenderDeferReadsHook = defineRule({
|
|
45700
44953
|
id: "rerender-defer-reads-hook",
|
|
45701
44954
|
title: "URL hook value only read in handlers",
|
|
@@ -45706,13 +44959,15 @@ const rerenderDeferReadsHook = defineRule({
|
|
|
45706
44959
|
create: (context) => {
|
|
45707
44960
|
const checkComponent = (componentBody) => {
|
|
45708
44961
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
45709
|
-
const bindings = findHookCallBindings(componentBody
|
|
44962
|
+
const bindings = findHookCallBindings(componentBody);
|
|
45710
44963
|
if (bindings.length === 0) return;
|
|
45711
44964
|
const handlerBindingNames = collectHandlerBindingNames(componentBody);
|
|
45712
44965
|
for (const binding of bindings) {
|
|
45713
|
-
const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
|
|
45714
44966
|
const referenceLocations = [];
|
|
45715
|
-
|
|
44967
|
+
walkAst(componentBody, (child) => {
|
|
44968
|
+
if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
|
|
44969
|
+
if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
|
|
44970
|
+
});
|
|
45716
44971
|
if (referenceLocations.length === 0) continue;
|
|
45717
44972
|
if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
|
|
45718
44973
|
context.report({
|
|
@@ -46083,10 +45338,14 @@ const rerenderLazyStateInit = defineRule({
|
|
|
46083
45338
|
//#endregion
|
|
46084
45339
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
46085
45340
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
46086
|
-
const callbackReturnsJsx = (callback
|
|
45341
|
+
const callbackReturnsJsx = (callback) => {
|
|
46087
45342
|
if (!callback) return false;
|
|
46088
45343
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
46089
|
-
|
|
45344
|
+
const body = callback.body;
|
|
45345
|
+
if (isJsxExpression(body)) return true;
|
|
45346
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45347
|
+
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45348
|
+
return false;
|
|
46090
45349
|
};
|
|
46091
45350
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
46092
45351
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -46164,7 +45423,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
46164
45423
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
46165
45424
|
for (const declarator of stmt.declarations ?? []) {
|
|
46166
45425
|
const init = declarator.init;
|
|
46167
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0]
|
|
45426
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
46168
45427
|
memoNode = declarator;
|
|
46169
45428
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
46170
45429
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -46230,11 +45489,8 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
46230
45489
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
46231
45490
|
const bindings = /* @__PURE__ */ new Map();
|
|
46232
45491
|
const params = functionNode.params ?? [];
|
|
46233
|
-
for (const
|
|
46234
|
-
|
|
46235
|
-
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
46236
|
-
}
|
|
46237
|
-
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45492
|
+
for (const param of params) collectFromObjectPattern(param, bindings);
|
|
45493
|
+
const propsParam = params[0];
|
|
46238
45494
|
const body = functionNode.body;
|
|
46239
45495
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
46240
45496
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -46740,8 +45996,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
46740
45996
|
if (statements.length !== 1) return;
|
|
46741
45997
|
const onlyStatement = statements[0];
|
|
46742
45998
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
46743
|
-
singleAssignment =
|
|
46744
|
-
} else if (body) singleAssignment =
|
|
45999
|
+
singleAssignment = onlyStatement.expression;
|
|
46000
|
+
} else if (body) singleAssignment = body;
|
|
46745
46001
|
if (!singleAssignment) return;
|
|
46746
46002
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
46747
46003
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|
|
@@ -53661,8 +52917,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
53661
52917
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
53662
52918
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
53663
52919
|
const firstArg = node.arguments?.[0];
|
|
53664
|
-
if (!
|
|
53665
|
-
if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
|
|
52920
|
+
if (!isNodeOfType(firstArg, "ObjectExpression")) return;
|
|
53666
52921
|
context.report({
|
|
53667
52922
|
node,
|
|
53668
52923
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|
|
@@ -54349,6 +53604,10 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
54349
53604
|
}
|
|
54350
53605
|
return false;
|
|
54351
53606
|
};
|
|
53607
|
+
const getJsxOpeningElementName = (node) => {
|
|
53608
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53609
|
+
return null;
|
|
53610
|
+
};
|
|
54352
53611
|
const stylePropObject = defineRule({
|
|
54353
53612
|
id: "style-prop-object",
|
|
54354
53613
|
title: "Style prop is not an object",
|
|
@@ -54360,8 +53619,7 @@ const stylePropObject = defineRule({
|
|
|
54360
53619
|
const allowSet = new Set(allow);
|
|
54361
53620
|
return {
|
|
54362
53621
|
JSXOpeningElement(node) {
|
|
54363
|
-
|
|
54364
|
-
const elementName = resolveJsxElementType(node);
|
|
53622
|
+
const elementName = getJsxOpeningElementName(node);
|
|
54365
53623
|
if (elementName && allowSet.has(elementName)) return;
|
|
54366
53624
|
if (elementName) {
|
|
54367
53625
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -55066,7 +54324,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
55066
54324
|
create: (context) => {
|
|
55067
54325
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
55068
54326
|
return { JSXOpeningElement(node) {
|
|
55069
|
-
if (
|
|
54327
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
55070
54328
|
const attributes = node.attributes ?? [];
|
|
55071
54329
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
55072
54330
|
if (!hrefAttribute?.value) return;
|
|
@@ -55687,7 +54945,8 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
55687
54945
|
create: (context) => ({
|
|
55688
54946
|
JSXElement(node) {
|
|
55689
54947
|
const openingElement = node.openingElement;
|
|
55690
|
-
|
|
54948
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
|
|
54949
|
+
const tagName = openingElement.name.name;
|
|
55691
54950
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
55692
54951
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
55693
54952
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|