oxlint-plugin-react-doctor 0.7.6-dev.17418 → 0.7.6-dev.1f14fa1
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 +692 -202
- 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
|
|
1184
|
-
const
|
|
1183
|
+
//#region src/plugin/utils/get-root-identifier.ts
|
|
1184
|
+
const getRootIdentifier$1 = (node, options) => {
|
|
1185
1185
|
if (!node) return null;
|
|
1186
1186
|
const followCallChains = options?.followCallChains === true;
|
|
1187
1187
|
let cursor = node;
|
|
@@ -1192,16 +1192,18 @@ const getRootIdentifierName = (node, options) => {
|
|
|
1192
1192
|
continue;
|
|
1193
1193
|
}
|
|
1194
1194
|
if (followCallChains && isNodeOfType(cursor, "CallExpression")) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
cursor = callee.object;
|
|
1195
|
+
if (!isNodeOfType(cursor.callee, "MemberExpression")) return null;
|
|
1196
|
+
cursor = cursor.callee.object;
|
|
1198
1197
|
continue;
|
|
1199
1198
|
}
|
|
1200
1199
|
break;
|
|
1201
1200
|
}
|
|
1202
|
-
return isNodeOfType(cursor, "Identifier") ? cursor
|
|
1201
|
+
return isNodeOfType(cursor, "Identifier") ? cursor : null;
|
|
1203
1202
|
};
|
|
1204
1203
|
//#endregion
|
|
1204
|
+
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1205
|
+
const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
|
|
1206
|
+
//#endregion
|
|
1205
1207
|
//#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
|
|
1206
1208
|
const STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
1207
1209
|
"useCallback",
|
|
@@ -1518,6 +1520,37 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1518
1520
|
}
|
|
1519
1521
|
};
|
|
1520
1522
|
//#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
|
|
1521
1554
|
//#region src/plugin/utils/get-element-type.ts
|
|
1522
1555
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1523
1556
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1530,14 +1563,8 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1530
1563
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1531
1564
|
return a11ySettings;
|
|
1532
1565
|
};
|
|
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
|
-
};
|
|
1539
1566
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1540
|
-
const baseName =
|
|
1567
|
+
const baseName = resolveJsxElementType(openingElement);
|
|
1541
1568
|
if (a11ySettings.polymorphicPropName) {
|
|
1542
1569
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1543
1570
|
if (polymorphicAttribute) {
|
|
@@ -2372,8 +2399,9 @@ const anchorIsValid = defineRule({
|
|
|
2372
2399
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2373
2400
|
return { JSXOpeningElement(node) {
|
|
2374
2401
|
if (isTestlikeFile) return;
|
|
2375
|
-
|
|
2376
|
-
if (
|
|
2402
|
+
const tag = getElementType(node, context.settings);
|
|
2403
|
+
if (!fileHasJsxA11ySettings && tag !== "a") return;
|
|
2404
|
+
if (tag !== "a") return;
|
|
2377
2405
|
let hrefAttribute;
|
|
2378
2406
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2379
2407
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5728,7 +5756,7 @@ const buttonHasType = defineRule({
|
|
|
5728
5756
|
return {
|
|
5729
5757
|
JSXOpeningElement(node) {
|
|
5730
5758
|
if (isTestlikeFile) return;
|
|
5731
|
-
if (
|
|
5759
|
+
if (resolveJsxElementType(node) !== "button") return;
|
|
5732
5760
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5733
5761
|
if (!typeAttr) {
|
|
5734
5762
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5918,7 +5946,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5918
5946
|
};
|
|
5919
5947
|
return {
|
|
5920
5948
|
JSXOpeningElement(node) {
|
|
5921
|
-
if (
|
|
5949
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
5922
5950
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5923
5951
|
},
|
|
5924
5952
|
CallExpression(node) {
|
|
@@ -5995,7 +6023,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5995
6023
|
//#endregion
|
|
5996
6024
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
5997
6025
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
5998
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6026
|
+
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
5999
6027
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6000
6028
|
let symbol = scopes.symbolFor(identifier);
|
|
6001
6029
|
while (symbol?.kind === "const") {
|
|
@@ -6270,6 +6298,131 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6270
6298
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6271
6299
|
};
|
|
6272
6300
|
//#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
|
|
6273
6426
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6274
6427
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6275
6428
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6291,26 +6444,39 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6291
6444
|
severity: "warn",
|
|
6292
6445
|
category: "Correctness",
|
|
6293
6446
|
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.",
|
|
6294
|
-
create: (context) =>
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6447
|
+
create: (context) => {
|
|
6448
|
+
let safelyValidatedStorageKeys = /* @__PURE__ */ new Set();
|
|
6449
|
+
return {
|
|
6450
|
+
Program(node) {
|
|
6451
|
+
safelyValidatedStorageKeys = collectSafelyValidatedLocalStorageKeys({
|
|
6452
|
+
programNode: node,
|
|
6453
|
+
scopes: context.scopes,
|
|
6454
|
+
resolveKey: (keyNode) => resolveStringKey(keyNode, context)
|
|
6455
|
+
});
|
|
6456
|
+
},
|
|
6457
|
+
CallExpression(node) {
|
|
6458
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6459
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6460
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6461
|
+
if (receiver.name !== "localStorage") return;
|
|
6462
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6463
|
+
if (node.callee.property.name !== "setItem") return;
|
|
6464
|
+
const keyArg = node.arguments?.[0];
|
|
6465
|
+
if (!keyArg) return;
|
|
6466
|
+
const keyValue = resolveStringKey(keyArg, context);
|
|
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
|
+
}
|
|
6314
6480
|
});
|
|
6315
6481
|
//#endregion
|
|
6316
6482
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -7146,12 +7312,17 @@ const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) =>
|
|
|
7146
7312
|
}
|
|
7147
7313
|
return nearestDeclaration;
|
|
7148
7314
|
};
|
|
7315
|
+
const isDeclarationStable = (identifier, declaration, usageIndex, fileContent) => {
|
|
7316
|
+
if (declaration.isImmutable) return true;
|
|
7317
|
+
const textAfterInitializer = fileContent.slice(declaration.initializerStartIndex + declaration.initializer.length, usageIndex);
|
|
7318
|
+
return !new RegExp(`(?:^|[^\\w$.])${escapeRegExp(identifier)}\\s*=(?!=)`).test(textAfterInitializer);
|
|
7319
|
+
};
|
|
7149
7320
|
const isTrustedHighlighterValue = (valueExpression, fileContent, sinkIndex) => {
|
|
7150
7321
|
if (!/highlight/i.test(valueExpression)) return false;
|
|
7151
7322
|
const identifier = valueExpression.match(/^([\w$]+)/)?.[1];
|
|
7152
7323
|
if (identifier === void 0) return false;
|
|
7153
7324
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7154
|
-
if (declaration !== null) return SERIALIZER_CALL_PROVENANCE_PATTERN.test(declaration.initializer);
|
|
7325
|
+
if (declaration !== null) return isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(declaration.initializer);
|
|
7155
7326
|
return /highlighted/i.test(valueExpression) || HIGHLIGHTER_LIBRARY_PATTERN.test(fileContent);
|
|
7156
7327
|
};
|
|
7157
7328
|
const isExplicitlyTrustedHtmlValue = (valueExpression, fileContent, sinkIndex, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
@@ -7163,7 +7334,7 @@ const isExplicitlyTrustedHtmlValue = (valueExpression, fileContent, sinkIndex, v
|
|
|
7163
7334
|
const identifier = trimmedExpression.match(/^([\w$]+)(?:(?:\??\.[\w$]+)|(?:\[\s*(?:"[^"\n]*"|'[^'\n]*'|\d+)\s*\]))*$/)?.[1];
|
|
7164
7335
|
if (identifier && !visitedIdentifiers.has(identifier)) {
|
|
7165
7336
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7166
|
-
if (declaration) {
|
|
7337
|
+
if (declaration && isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) {
|
|
7167
7338
|
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7168
7339
|
nextVisitedIdentifiers.add(identifier);
|
|
7169
7340
|
return isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex, nextVisitedIdentifiers);
|
|
@@ -7230,6 +7401,16 @@ const splitTopLevelArguments = (argumentText) => {
|
|
|
7230
7401
|
argumentsList.push(argumentText.slice(startIndex).trim());
|
|
7231
7402
|
return argumentsList;
|
|
7232
7403
|
};
|
|
7404
|
+
const isBackedByFunctionParameter = (expression, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7405
|
+
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
7406
|
+
if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
|
|
7407
|
+
if (findContainingFunctionParameterSource(identifier, expressionIndex, fileContent) !== null) return true;
|
|
7408
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
|
|
7409
|
+
if (!declaration || !isDeclarationStable(identifier, declaration, expressionIndex, fileContent)) return false;
|
|
7410
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7411
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7412
|
+
return isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
|
|
7413
|
+
};
|
|
7233
7414
|
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers, visitedCallSites) => {
|
|
7234
7415
|
const trimmedExpression = expression.trim();
|
|
7235
7416
|
if (isExplicitlyTrustedHtmlValue(trimmedExpression, fileContent, sinkIndex)) return false;
|
|
@@ -7240,8 +7421,11 @@ const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers, v
|
|
|
7240
7421
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7241
7422
|
const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
|
|
7242
7423
|
if (declaration !== null && (parameterSource === null || declaration.declarationIndex > parameterSource.declarationNameIndex)) {
|
|
7243
|
-
if (isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex)) return false;
|
|
7244
|
-
|
|
7424
|
+
if (isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex)) return false;
|
|
7425
|
+
if (!isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) return true;
|
|
7426
|
+
if (isHtmlTainted(declaration.initializer, fileContent, declaration.initializerStartIndex, visitedIdentifiers, visitedCallSites)) return true;
|
|
7427
|
+
if (isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent)) return false;
|
|
7428
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7245
7429
|
}
|
|
7246
7430
|
if (parameterSource !== null && parameterSource.functionName.length > 0) {
|
|
7247
7431
|
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(`, "g");
|
|
@@ -7323,12 +7507,13 @@ const dangerousHtmlSink = defineRule({
|
|
|
7323
7507
|
if (isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
|
|
7324
7508
|
if (valueIdentifier !== void 0) {
|
|
7325
7509
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
7326
|
-
const
|
|
7510
|
+
const visibleDeclaration = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content);
|
|
7511
|
+
const visibleInitializer = visibleDeclaration?.initializer;
|
|
7327
7512
|
if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
|
|
7328
7513
|
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7329
|
-
if (visibleInitializer === void 0 ? fromSerializer.test(file.content) : SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer)) continue;
|
|
7514
|
+
if (visibleInitializer === void 0 ? fromSerializer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer)) continue;
|
|
7330
7515
|
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7331
|
-
if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`)) continue;
|
|
7516
|
+
if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`)) continue;
|
|
7332
7517
|
}
|
|
7333
7518
|
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
7334
7519
|
if (new RegExp(`highlight[\\w$]*\\s*\\.map\\(\\s*(?:async\\s+)?\\(?\\s*${escapedIdentifier}\\b`, "i").test(file.content) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
@@ -7414,7 +7599,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7414
7599
|
const getOpeningElementTagName = (openingElement) => {
|
|
7415
7600
|
if (!openingElement) return null;
|
|
7416
7601
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7417
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement
|
|
7602
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return resolveJsxElementType(openingElement);
|
|
7418
7603
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7419
7604
|
let cursor = openingElement.name;
|
|
7420
7605
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7666,7 +7851,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7666
7851
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7667
7852
|
return { JSXOpeningElement(node) {
|
|
7668
7853
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7669
|
-
const tagName = node
|
|
7854
|
+
const tagName = resolveJsxElementType(node);
|
|
7670
7855
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7671
7856
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7672
7857
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -11898,7 +12083,7 @@ const forbidDomProps = defineRule({
|
|
|
11898
12083
|
return { JSXOpeningElement(node) {
|
|
11899
12084
|
if (forbidMap.size === 0) return;
|
|
11900
12085
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
11901
|
-
const elementName = node
|
|
12086
|
+
const elementName = resolveJsxElementType(node);
|
|
11902
12087
|
if (isReactComponentName(elementName)) return;
|
|
11903
12088
|
for (const attribute of node.attributes) {
|
|
11904
12089
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11976,7 +12161,7 @@ const forbidElements = defineRule({
|
|
|
11976
12161
|
return {
|
|
11977
12162
|
JSXOpeningElement(node) {
|
|
11978
12163
|
if (forbidMap.size === 0) return;
|
|
11979
|
-
const fullName =
|
|
12164
|
+
const fullName = resolveJsxElementType(node);
|
|
11980
12165
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
11981
12166
|
context.report({
|
|
11982
12167
|
node: node.name,
|
|
@@ -12338,8 +12523,7 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12338
12523
|
const isParagraphElement = (candidate) => {
|
|
12339
12524
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12340
12525
|
const opening = candidate.openingElement;
|
|
12341
|
-
|
|
12342
|
-
return opening.name.name === "p";
|
|
12526
|
+
return resolveJsxElementType(opening) === "p";
|
|
12343
12527
|
};
|
|
12344
12528
|
const findEnclosingParagraph = (openingElement) => {
|
|
12345
12529
|
const owningElement = openingElement.parent;
|
|
@@ -12360,8 +12544,7 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12360
12544
|
severity: "warn",
|
|
12361
12545
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12362
12546
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12363
|
-
|
|
12364
|
-
const childTagName = node.name.name;
|
|
12547
|
+
const childTagName = resolveJsxElementType(node);
|
|
12365
12548
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12366
12549
|
if (!findEnclosingParagraph(node)) return;
|
|
12367
12550
|
context.report({
|
|
@@ -12392,7 +12575,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12392
12575
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12393
12576
|
const opening = jsxElement.openingElement;
|
|
12394
12577
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12395
|
-
const tagName = opening
|
|
12578
|
+
const tagName = resolveJsxElementType(opening);
|
|
12396
12579
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12397
12580
|
return tagName;
|
|
12398
12581
|
};
|
|
@@ -12422,7 +12605,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12422
12605
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12423
12606
|
const opening = ancestor.openingElement;
|
|
12424
12607
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12425
|
-
const ancestorTag = opening
|
|
12608
|
+
const ancestorTag = resolveJsxElementType(opening);
|
|
12426
12609
|
if (ancestorTag.length === 0) {
|
|
12427
12610
|
previous = ancestor;
|
|
12428
12611
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12504,8 +12687,7 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12504
12687
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12505
12688
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12506
12689
|
const opening = candidate.openingElement;
|
|
12507
|
-
|
|
12508
|
-
return opening.name.name === tagName;
|
|
12690
|
+
return resolveJsxElementType(opening) === tagName;
|
|
12509
12691
|
};
|
|
12510
12692
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12511
12693
|
const owningElement = openingElement.parent;
|
|
@@ -12526,8 +12708,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12526
12708
|
severity: "warn",
|
|
12527
12709
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12528
12710
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12529
|
-
|
|
12530
|
-
const tagName = node.name.name;
|
|
12711
|
+
const tagName = resolveJsxElementType(node);
|
|
12531
12712
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12532
12713
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12533
12714
|
context.report({
|
|
@@ -12642,7 +12823,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12642
12823
|
matchByOccurrence: true,
|
|
12643
12824
|
create: skipNonProductionFiles((context) => ({
|
|
12644
12825
|
JSXOpeningElement(node) {
|
|
12645
|
-
if (
|
|
12826
|
+
if (resolveJsxElementType(node) !== "iframe") return;
|
|
12646
12827
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12647
12828
|
if (!sandboxAttr) {
|
|
12648
12829
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -13634,6 +13815,28 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13634
13815
|
} })
|
|
13635
13816
|
});
|
|
13636
13817
|
//#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
|
|
13637
13840
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13638
13841
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13639
13842
|
"forEach",
|
|
@@ -13818,9 +14021,19 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
13818
14021
|
});
|
|
13819
14022
|
return foundAttachment;
|
|
13820
14023
|
};
|
|
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
|
+
};
|
|
13821
14033
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
13822
|
-
|
|
13823
|
-
|
|
14034
|
+
const assignment = getStyleAssignment(styleWriteStatement);
|
|
14035
|
+
if (!assignment || !isNodeOfType(assignment.left, "MemberExpression") || !isNodeOfType(assignment.left.object, "MemberExpression")) return false;
|
|
14036
|
+
const elementExpression = assignment.left.object.object;
|
|
13824
14037
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
13825
14038
|
if (!creationRoot) return false;
|
|
13826
14039
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -13832,15 +14045,17 @@ const jsBatchDomCss = defineRule({
|
|
|
13832
14045
|
severity: "warn",
|
|
13833
14046
|
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",
|
|
13834
14047
|
create: (context) => {
|
|
13835
|
-
const
|
|
13836
|
-
|
|
14048
|
+
const writesLayoutAffectingProperty = (node) => {
|
|
14049
|
+
const assignment = getStyleAssignment(node);
|
|
14050
|
+
return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
|
|
14051
|
+
};
|
|
13837
14052
|
return { BlockStatement(node) {
|
|
13838
14053
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
13839
14054
|
if (!perIterationBody) return;
|
|
13840
14055
|
const statements = node.body ?? [];
|
|
13841
14056
|
let layoutReads = null;
|
|
13842
14057
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
13843
|
-
if (
|
|
14058
|
+
if (getStyleAssignment(statements[statementIndex]) === null || getStyleAssignment(statements[statementIndex - 1]) === null) continue;
|
|
13844
14059
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
13845
14060
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
13846
14061
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -14536,6 +14751,18 @@ const jsHoistRegexp = defineRule({
|
|
|
14536
14751
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14537
14752
|
});
|
|
14538
14753
|
//#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
|
|
14539
14766
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14540
14767
|
const referencesParameter = (expression, parameterName) => {
|
|
14541
14768
|
if (!expression) return false;
|
|
@@ -14546,7 +14773,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14546
14773
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14547
14774
|
const callback = node.arguments?.[0];
|
|
14548
14775
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14549
|
-
const firstParameter = callback.params?.[0];
|
|
14776
|
+
const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
|
|
14550
14777
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14551
14778
|
let predicate = null;
|
|
14552
14779
|
const body = callback.body;
|
|
@@ -15238,9 +15465,21 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15238
15465
|
};
|
|
15239
15466
|
const getResolvedInitializer = (receiver) => {
|
|
15240
15467
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15241
|
-
const
|
|
15242
|
-
|
|
15243
|
-
return
|
|
15468
|
+
const binding = findVariableInitializer(receiver, receiver.name);
|
|
15469
|
+
const initializer = binding?.initializer ?? null;
|
|
15470
|
+
if (!binding || !initializer) return null;
|
|
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
|
+
};
|
|
15244
15483
|
};
|
|
15245
15484
|
const isSplitCall = (expression) => {
|
|
15246
15485
|
if (!expression) return false;
|
|
@@ -15406,8 +15645,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15406
15645
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15407
15646
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15408
15647
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15409
|
-
const
|
|
15410
|
-
if (
|
|
15648
|
+
const resolved = getResolvedInitializer(stripped);
|
|
15649
|
+
if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
|
|
15411
15650
|
}
|
|
15412
15651
|
return false;
|
|
15413
15652
|
};
|
|
@@ -15459,8 +15698,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15459
15698
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15460
15699
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15461
15700
|
if (resolvedInitializer) {
|
|
15462
|
-
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15463
|
-
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15701
|
+
if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
|
|
15702
|
+
if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
|
|
15464
15703
|
}
|
|
15465
15704
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15466
15705
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -15873,14 +16112,21 @@ const jsxFilenameExtension = defineRule({
|
|
|
15873
16112
|
});
|
|
15874
16113
|
//#endregion
|
|
15875
16114
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
15876
|
-
const isJsxFragmentElement = (node) => {
|
|
16115
|
+
const isJsxFragmentElement = (node, scopes) => {
|
|
15877
16116
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
15878
16117
|
const elementName = node.name;
|
|
15879
|
-
if (isNodeOfType(elementName, "JSXIdentifier"))
|
|
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
|
+
}
|
|
15880
16124
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
15881
16125
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
15882
|
-
if (elementName.
|
|
15883
|
-
return elementName.
|
|
16126
|
+
if (elementName.property.name !== "Fragment") return false;
|
|
16127
|
+
if (!scopes) return elementName.object.name === "React";
|
|
16128
|
+
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
16129
|
+
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15884
16130
|
}
|
|
15885
16131
|
return false;
|
|
15886
16132
|
};
|
|
@@ -15906,7 +16152,7 @@ const jsxFragments = defineRule({
|
|
|
15906
16152
|
if (mode !== "syntax") return;
|
|
15907
16153
|
if (!node.closingElement) return;
|
|
15908
16154
|
const openingElement = node.openingElement;
|
|
15909
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
16155
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
15910
16156
|
if (openingElement.attributes.length > 0) return;
|
|
15911
16157
|
context.report({
|
|
15912
16158
|
node: openingElement,
|
|
@@ -18587,10 +18833,6 @@ const resolveSettings$29 = (settings) => {
|
|
|
18587
18833
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18588
18834
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18589
18835
|
};
|
|
18590
|
-
const getElementName = (node) => {
|
|
18591
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18592
|
-
return null;
|
|
18593
|
-
};
|
|
18594
18836
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18595
18837
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18596
18838
|
const explicit = options.components?.[elementName];
|
|
@@ -18610,7 +18852,8 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18610
18852
|
create: (context) => {
|
|
18611
18853
|
const options = resolveSettings$29(context.settings);
|
|
18612
18854
|
return { JSXOpeningElement(node) {
|
|
18613
|
-
|
|
18855
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18856
|
+
const elementName = resolveJsxElementType(node);
|
|
18614
18857
|
if (!elementName) return;
|
|
18615
18858
|
for (const attribute of node.attributes) {
|
|
18616
18859
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18797,11 +19040,6 @@ const checkTarget = (attributeValue) => {
|
|
|
18797
19040
|
alternate: false
|
|
18798
19041
|
};
|
|
18799
19042
|
};
|
|
18800
|
-
const getOpeningElementName = (node) => {
|
|
18801
|
-
const name = node.name;
|
|
18802
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18803
|
-
return null;
|
|
18804
|
-
};
|
|
18805
19043
|
const jsxNoTargetBlank = defineRule({
|
|
18806
19044
|
id: "jsx-no-target-blank",
|
|
18807
19045
|
title: "Unsafe target=_blank link",
|
|
@@ -18821,7 +19059,8 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18821
19059
|
return settings.formComponents.has(tagName);
|
|
18822
19060
|
};
|
|
18823
19061
|
return { JSXOpeningElement(node) {
|
|
18824
|
-
|
|
19062
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
19063
|
+
const tagName = resolveJsxElementType(node);
|
|
18825
19064
|
if (!tagName) return;
|
|
18826
19065
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18827
19066
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19054,7 +19293,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19054
19293
|
return {
|
|
19055
19294
|
JSXElement(node) {
|
|
19056
19295
|
const openingElement = node.openingElement;
|
|
19057
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
19296
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
19058
19297
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19059
19298
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19060
19299
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20296,11 +20535,24 @@ const hasDirective = (programNode, directive) => {
|
|
|
20296
20535
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20297
20536
|
};
|
|
20298
20537
|
//#endregion
|
|
20299
|
-
//#region src/plugin/utils/
|
|
20300
|
-
const
|
|
20301
|
-
|
|
20302
|
-
|
|
20303
|
-
|
|
20538
|
+
//#region src/plugin/utils/unwrap-object-integrity-expression.ts
|
|
20539
|
+
const OBJECT_INTEGRITY_METHOD_NAMES = new Set([
|
|
20540
|
+
"freeze",
|
|
20541
|
+
"seal",
|
|
20542
|
+
"preventExtensions"
|
|
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
|
+
};
|
|
20304
20556
|
//#endregion
|
|
20305
20557
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20306
20558
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20326,10 +20578,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20326
20578
|
},
|
|
20327
20579
|
VariableDeclarator(node) {
|
|
20328
20580
|
if (!fileHasUseClient) return;
|
|
20329
|
-
if (!isComponentAssignment(node)) return;
|
|
20330
|
-
if (!isInlineFunctionExpression(node.init)) return;
|
|
20331
|
-
if (!node.init.async) return;
|
|
20332
20581
|
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;
|
|
20333
20585
|
context.report({
|
|
20334
20586
|
node,
|
|
20335
20587
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -20631,7 +20883,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20631
20883
|
severity: "warn",
|
|
20632
20884
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20633
20885
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20634
|
-
if (
|
|
20886
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
20635
20887
|
const attributes = node.attributes ?? [];
|
|
20636
20888
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20637
20889
|
if (downloadAttribute) {
|
|
@@ -20827,7 +21079,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20827
21079
|
severity: "warn",
|
|
20828
21080
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20829
21081
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20830
|
-
if (
|
|
21082
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20831
21083
|
const attributes = node.attributes ?? [];
|
|
20832
21084
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20833
21085
|
if (!relAttribute?.value) return;
|
|
@@ -20935,7 +21187,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
20935
21187
|
severity: "warn",
|
|
20936
21188
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
20937
21189
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20938
|
-
if (
|
|
21190
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20939
21191
|
const attributes = node.attributes ?? [];
|
|
20940
21192
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
20941
21193
|
if (!hrefAttribute?.value) return;
|
|
@@ -21110,7 +21362,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21110
21362
|
create: (context) => {
|
|
21111
21363
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21112
21364
|
return { JSXOpeningElement(node) {
|
|
21113
|
-
if (
|
|
21365
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
21114
21366
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21115
21367
|
const programRoot = findProgramRoot(node);
|
|
21116
21368
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21157,8 +21409,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21157
21409
|
severity: "warn",
|
|
21158
21410
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21159
21411
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21160
|
-
|
|
21161
|
-
if (
|
|
21412
|
+
const elementName = resolveJsxElementType(node);
|
|
21413
|
+
if (elementName !== "script" && elementName !== "Script") return;
|
|
21162
21414
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21163
21415
|
if (!srcAttribute?.value) return;
|
|
21164
21416
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -22966,10 +23218,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
22966
23218
|
if (!declaringNode) return false;
|
|
22967
23219
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
22968
23220
|
}));
|
|
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
|
+
};
|
|
22969
23227
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
22970
23228
|
if (def.type !== "Parameter") return false;
|
|
22971
|
-
|
|
22972
|
-
return isFunctionLike$1(bindingParent);
|
|
23229
|
+
return isWholePropsParameterBinding(def.name);
|
|
22973
23230
|
}));
|
|
22974
23231
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
22975
23232
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -25966,6 +26223,56 @@ const noBarrelImport = defineRule({
|
|
|
25966
26223
|
}
|
|
25967
26224
|
});
|
|
25968
26225
|
//#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
|
|
25969
26276
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
25970
26277
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
25971
26278
|
"FunctionDeclaration",
|
|
@@ -25985,12 +26292,13 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
25985
26292
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
25986
26293
|
};
|
|
25987
26294
|
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);
|
|
25988
26296
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
25989
26297
|
let hasRenderOutput = false;
|
|
25990
26298
|
walkAst(rootNode, (node) => {
|
|
25991
26299
|
if (hasRenderOutput) return false;
|
|
25992
26300
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
25993
|
-
if (
|
|
26301
|
+
if (isRenderOutputExpression(node, scopes)) {
|
|
25994
26302
|
hasRenderOutput = true;
|
|
25995
26303
|
return false;
|
|
25996
26304
|
}
|
|
@@ -26001,7 +26309,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26001
26309
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26002
26310
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26003
26311
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26004
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26312
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
|
|
26005
26313
|
renderOutputCache.set(functionNode, {
|
|
26006
26314
|
scopes,
|
|
26007
26315
|
hasRenderOutput
|
|
@@ -26009,6 +26317,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
26009
26317
|
return hasRenderOutput;
|
|
26010
26318
|
};
|
|
26011
26319
|
//#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
|
|
26012
26323
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
26013
26324
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
26014
26325
|
//#endregion
|
|
@@ -28466,7 +28777,7 @@ const noDisabledZoom = defineRule({
|
|
|
28466
28777
|
category: "Accessibility",
|
|
28467
28778
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28468
28779
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28469
|
-
if (
|
|
28780
|
+
if (resolveJsxElementType(node) !== "meta") return;
|
|
28470
28781
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28471
28782
|
if (!nameAttr?.value) return;
|
|
28472
28783
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -28856,7 +29167,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
28856
29167
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
28857
29168
|
for (const statement of componentBody.body ?? []) {
|
|
28858
29169
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
28859
|
-
const expression = statement
|
|
29170
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
28860
29171
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
28861
29172
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
28862
29173
|
effectCalls.push(expression);
|
|
@@ -31638,7 +31949,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31638
31949
|
severity: "warn",
|
|
31639
31950
|
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.",
|
|
31640
31951
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31641
|
-
if (
|
|
31952
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
31642
31953
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31643
31954
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31644
31955
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31650,6 +31961,26 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31650
31961
|
} })
|
|
31651
31962
|
});
|
|
31652
31963
|
//#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
|
|
31653
31984
|
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
31654
31985
|
const TIMER_FUNCTION_NAMES = new Set([
|
|
31655
31986
|
"cancelAnimationFrame",
|
|
@@ -31681,6 +32012,33 @@ const NOTIFICATION_METHOD_NAMES = new Set([
|
|
|
31681
32012
|
"success",
|
|
31682
32013
|
"warning"
|
|
31683
32014
|
]);
|
|
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
|
+
};
|
|
31684
32042
|
const getMemberCall = (node) => {
|
|
31685
32043
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
31686
32044
|
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
@@ -31692,27 +32050,37 @@ const getMemberCall = (node) => {
|
|
|
31692
32050
|
};
|
|
31693
32051
|
const isNotificationReceiver = (receiver, scopes) => {
|
|
31694
32052
|
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
31695
|
-
|
|
32053
|
+
const importBinding = getImportBindingForName(receiver, receiver.name);
|
|
32054
|
+
if (importBinding) return isNotificationModuleSource(importBinding.source) && (importBinding.exportedName === "default" || importBinding.exportedName !== null && NOTIFICATION_RECEIVER_NAMES.has(importBinding.exportedName));
|
|
31696
32055
|
const symbol = scopes.symbolFor(receiver);
|
|
31697
|
-
if (symbol?.kind === "import") return true;
|
|
31698
32056
|
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
31699
32057
|
const callee = symbol.initializer.callee;
|
|
31700
|
-
|
|
32058
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
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;
|
|
31701
32068
|
};
|
|
31702
32069
|
const getKnownImpureCall = (callExpression, scopes) => {
|
|
31703
32070
|
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}()`;
|
|
31704
32072
|
const memberCall = getMemberCall(callExpression);
|
|
31705
32073
|
if (!memberCall) return null;
|
|
31706
32074
|
const { methodName, receiver } = memberCall;
|
|
31707
|
-
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) &&
|
|
31708
|
-
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
31709
|
-
|
|
32075
|
+
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isStorageReceiver(receiver, scopes)) return `${methodName}()`;
|
|
32076
|
+
if (EXTERNAL_READ_METHOD_NAMES.has(methodName) && hasReactRefCurrentOrigin(receiver, scopes)) return `.${methodName}()`;
|
|
32077
|
+
const receiverRoot = getRootIdentifier$1(receiver);
|
|
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}()`;
|
|
31710
32080
|
return null;
|
|
31711
32081
|
};
|
|
31712
32082
|
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
31713
|
-
|
|
31714
|
-
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
31715
|
-
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
32083
|
+
const rootIdentifier = getRootIdentifier$1(assignmentTarget);
|
|
31716
32084
|
if (!rootIdentifier) return null;
|
|
31717
32085
|
const updaterScope = scopes.ownScopeFor(updater);
|
|
31718
32086
|
if (!updaterScope) return null;
|
|
@@ -31721,12 +32089,26 @@ const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) =>
|
|
|
31721
32089
|
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
31722
32090
|
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
31723
32091
|
};
|
|
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
|
+
};
|
|
31724
32106
|
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
31725
32107
|
const analysis = getProgramAnalysis(updater);
|
|
31726
32108
|
let operation = null;
|
|
31727
32109
|
walkAst(updater, (child) => {
|
|
31728
32110
|
if (operation) return false;
|
|
31729
|
-
if (child !== updater && isFunctionLike$1(child) && !
|
|
32111
|
+
if (child !== updater && isFunctionLike$1(child) && !isDefinitelySynchronousCallback(child, scopes)) return false;
|
|
31730
32112
|
if (isNodeOfType(child, "CallExpression")) {
|
|
31731
32113
|
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
31732
32114
|
const calleeReference = getRef(analysis, child.callee);
|
|
@@ -31758,18 +32140,25 @@ const noImpureStateUpdater = defineRule({
|
|
|
31758
32140
|
severity: "error",
|
|
31759
32141
|
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.",
|
|
31760
32142
|
create: (context) => ({ CallExpression(node) {
|
|
31761
|
-
const
|
|
31762
|
-
if (!
|
|
32143
|
+
const updaterArgument = node.arguments?.[0];
|
|
32144
|
+
if (!updaterArgument) return;
|
|
31763
32145
|
const analysis = getProgramAnalysis(node);
|
|
31764
32146
|
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
31765
32147
|
const calleeReference = getRef(analysis, node.callee);
|
|
31766
32148
|
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
31767
32149
|
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
31768
32150
|
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;
|
|
31769
32158
|
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
31770
32159
|
if (!operation) return;
|
|
31771
32160
|
context.report({
|
|
31772
|
-
node:
|
|
32161
|
+
node: updaterArgument,
|
|
31773
32162
|
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.`
|
|
31774
32163
|
});
|
|
31775
32164
|
} })
|
|
@@ -31879,7 +32268,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
31879
32268
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
31880
32269
|
return {
|
|
31881
32270
|
JSXOpeningElement(node) {
|
|
31882
|
-
if (
|
|
32271
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
31883
32272
|
let typeAttribute = null;
|
|
31884
32273
|
let typeAttributeIndex = null;
|
|
31885
32274
|
let indeterminateAttribute = null;
|
|
@@ -32058,11 +32447,12 @@ const isMemoCall = (node) => {
|
|
|
32058
32447
|
};
|
|
32059
32448
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
32060
32449
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
32061
|
-
const isInlineReference = (node) => {
|
|
32062
|
-
|
|
32063
|
-
if (isNodeOfType(
|
|
32064
|
-
if (isNodeOfType(
|
|
32065
|
-
if (isNodeOfType(
|
|
32450
|
+
const isInlineReference = (node, scopes) => {
|
|
32451
|
+
const referenceNode = unwrapObjectIntegrityExpression(node, scopes);
|
|
32452
|
+
if (isNodeOfType(referenceNode, "ArrowFunctionExpression") || isNodeOfType(referenceNode, "FunctionExpression") || isNodeOfType(referenceNode, "CallExpression") && isNodeOfType(referenceNode.callee, "MemberExpression") && isNodeOfType(referenceNode.callee.property, "Identifier") && referenceNode.callee.property.name === "bind") return "functions";
|
|
32453
|
+
if (isNodeOfType(referenceNode, "ObjectExpression")) return "objects";
|
|
32454
|
+
if (isNodeOfType(referenceNode, "ArrayExpression")) return "Arrays";
|
|
32455
|
+
if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
|
|
32066
32456
|
return null;
|
|
32067
32457
|
};
|
|
32068
32458
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -32092,7 +32482,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
32092
32482
|
let elementName = null;
|
|
32093
32483
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
32094
32484
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
32095
|
-
const propType = isInlineReference(node.value.expression);
|
|
32485
|
+
const propType = isInlineReference(node.value.expression, context.scopes);
|
|
32096
32486
|
if (propType) context.report({
|
|
32097
32487
|
node: node.value.expression,
|
|
32098
32488
|
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`
|
|
@@ -32951,11 +33341,13 @@ const noManyBooleanProps = defineRule({
|
|
|
32951
33341
|
};
|
|
32952
33342
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
32953
33343
|
if (!param) return;
|
|
33344
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33345
|
+
if (!propsBinding) return;
|
|
32954
33346
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
32955
|
-
if (isNodeOfType(
|
|
33347
|
+
if (isNodeOfType(propsBinding, "ObjectPattern")) {
|
|
32956
33348
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
32957
33349
|
const booleanLikePropNames = [];
|
|
32958
|
-
for (const property of
|
|
33350
|
+
for (const property of propsBinding.properties ?? []) {
|
|
32959
33351
|
if (!isNodeOfType(property, "Property")) continue;
|
|
32960
33352
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
32961
33353
|
if (!keyName) continue;
|
|
@@ -32966,7 +33358,7 @@ const noManyBooleanProps = defineRule({
|
|
|
32966
33358
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
32967
33359
|
return;
|
|
32968
33360
|
}
|
|
32969
|
-
if (isNodeOfType(
|
|
33361
|
+
if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
|
|
32970
33362
|
};
|
|
32971
33363
|
return {
|
|
32972
33364
|
FunctionDeclaration(node) {
|
|
@@ -33088,7 +33480,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33088
33480
|
if (mirrorBindings.length === 0) return;
|
|
33089
33481
|
for (const statement of componentBody.body ?? []) {
|
|
33090
33482
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
33091
|
-
const effectCall = statement
|
|
33483
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
33092
33484
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
33093
33485
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
33094
33486
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -33102,7 +33494,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33102
33494
|
const bodyStatements = getCallbackStatements(callback);
|
|
33103
33495
|
if (bodyStatements.length !== 1) continue;
|
|
33104
33496
|
const onlyStatement = bodyStatements[0];
|
|
33105
|
-
const expression =
|
|
33497
|
+
const expression = unwrapDiscardedExpression(onlyStatement);
|
|
33106
33498
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
33107
33499
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
33108
33500
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -35718,7 +36110,7 @@ const noPreventDefault = defineRule({
|
|
|
35718
36110
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35719
36111
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35720
36112
|
return { JSXOpeningElement(node) {
|
|
35721
|
-
const elementName =
|
|
36113
|
+
const elementName = resolveJsxElementType(node);
|
|
35722
36114
|
if (!elementName) return;
|
|
35723
36115
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35724
36116
|
if (!targetEventProps) return;
|
|
@@ -36608,28 +37000,85 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
36608
37000
|
});
|
|
36609
37001
|
//#endregion
|
|
36610
37002
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
36611
|
-
const
|
|
36612
|
-
|
|
36613
|
-
|
|
36614
|
-
|
|
36615
|
-
|
|
36616
|
-
|
|
36617
|
-
|
|
36618
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
36619
|
-
};
|
|
37003
|
+
const REPEATED_ANCESTOR_TYPES = new Set([
|
|
37004
|
+
"DoWhileStatement",
|
|
37005
|
+
"ForInStatement",
|
|
37006
|
+
"ForOfStatement",
|
|
37007
|
+
"ForStatement",
|
|
37008
|
+
"WhileStatement"
|
|
37009
|
+
]);
|
|
36620
37010
|
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
36621
|
-
if (!isNodeOfType(node, "MemberExpression") ||
|
|
37011
|
+
if (!isNodeOfType(node, "MemberExpression") || getStaticPropertyName(node) !== "current") return false;
|
|
36622
37012
|
const receiver = stripParenExpression(node.object);
|
|
36623
37013
|
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
36624
37014
|
};
|
|
37015
|
+
const isSameRefCurrentAlias = (node, refSymbol, scopes) => {
|
|
37016
|
+
if (isSameRefCurrentMember(node, refSymbol, scopes)) return true;
|
|
37017
|
+
if (!isNodeOfType(node, "Identifier")) return false;
|
|
37018
|
+
const aliasSymbol = scopes.symbolFor(node);
|
|
37019
|
+
return aliasSymbol?.kind === "const" && aliasSymbol.initializer !== null && isSameRefCurrentMember(stripParenExpression(aliasSymbol.initializer), refSymbol, scopes);
|
|
37020
|
+
};
|
|
37021
|
+
const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
|
|
37022
|
+
const hasRepeatedExecutionAncestor = (node, stop) => {
|
|
37023
|
+
let ancestor = node.parent;
|
|
37024
|
+
while (ancestor && ancestor !== stop) {
|
|
37025
|
+
if (isFunctionLike$1(ancestor) || REPEATED_ANCESTOR_TYPES.has(ancestor.type)) return true;
|
|
37026
|
+
ancestor = ancestor.parent;
|
|
37027
|
+
}
|
|
37028
|
+
return ancestor !== stop;
|
|
37029
|
+
};
|
|
37030
|
+
const getBranchConstraints = (node, branchRoot) => {
|
|
37031
|
+
const constraints = /* @__PURE__ */ new Map();
|
|
37032
|
+
let descendant = node;
|
|
37033
|
+
let ancestor = descendant.parent;
|
|
37034
|
+
while (ancestor && descendant !== branchRoot) {
|
|
37035
|
+
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
37036
|
+
if (ancestor.consequent === descendant) constraints.set(ancestor, true);
|
|
37037
|
+
if (ancestor.alternate === descendant) constraints.set(ancestor, false);
|
|
37038
|
+
}
|
|
37039
|
+
descendant = ancestor;
|
|
37040
|
+
ancestor = ancestor.parent;
|
|
37041
|
+
}
|
|
37042
|
+
return constraints;
|
|
37043
|
+
};
|
|
37044
|
+
const canExecuteTogether = (firstConstraints, secondConstraints) => {
|
|
37045
|
+
for (const [statement, branch] of firstConstraints) {
|
|
37046
|
+
const otherBranch = secondConstraints.get(statement);
|
|
37047
|
+
if (otherBranch !== void 0 && otherBranch !== branch) return false;
|
|
37048
|
+
}
|
|
37049
|
+
return true;
|
|
37050
|
+
};
|
|
37051
|
+
const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol, scopes) => {
|
|
37052
|
+
const assignmentConstraints = getBranchConstraints(assignmentExpression, branchRoot);
|
|
37053
|
+
const assignmentStart = getRangeStart(assignmentExpression);
|
|
37054
|
+
let hasCoExecutableWrite = false;
|
|
37055
|
+
walkAst(branchRoot, (child) => {
|
|
37056
|
+
if (hasCoExecutableWrite) return false;
|
|
37057
|
+
const childStart = getRangeStart(child);
|
|
37058
|
+
if (child === assignmentExpression || !isNodeOfType(child, "AssignmentExpression") || assignmentStart === null || childStart === null || childStart >= assignmentStart || resolveReactRefSymbol(child.left, scopes)?.id !== refSymbol.id || hasRepeatedExecutionAncestor(child, branchRoot)) return;
|
|
37059
|
+
if (canExecuteTogether(assignmentConstraints, getBranchConstraints(child, branchRoot))) {
|
|
37060
|
+
hasCoExecutableWrite = true;
|
|
37061
|
+
return false;
|
|
37062
|
+
}
|
|
37063
|
+
});
|
|
37064
|
+
return !hasCoExecutableWrite;
|
|
37065
|
+
};
|
|
36625
37066
|
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
36626
|
-
if (assignmentExpression.operator
|
|
37067
|
+
if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return true;
|
|
37068
|
+
if (assignmentExpression.operator !== "=") return false;
|
|
36627
37069
|
let descendant = assignmentExpression;
|
|
36628
37070
|
let ancestor = descendant.parent;
|
|
36629
37071
|
while (ancestor) {
|
|
36630
|
-
if (isNodeOfType(ancestor, "IfStatement") &&
|
|
37072
|
+
if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(ancestor.test, "BinaryExpression") && [
|
|
37073
|
+
"===",
|
|
37074
|
+
"==",
|
|
37075
|
+
"!==",
|
|
37076
|
+
"!="
|
|
37077
|
+
].includes(ancestor.test.operator)) {
|
|
36631
37078
|
const { left, right } = ancestor.test;
|
|
36632
|
-
|
|
37079
|
+
const comparesEmptySentinel = isSameRefCurrentAlias(left, refSymbol, scopes) && isEmptySentinel(right, scopes) || isSameRefCurrentAlias(right, refSymbol, scopes) && isEmptySentinel(left, scopes);
|
|
37080
|
+
const guardedBranch = ancestor.test.operator === "===" || ancestor.test.operator === "==" ? ancestor.consequent : ancestor.alternate;
|
|
37081
|
+
if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && hasNoPriorCoExecutableWrite(assignmentExpression, guardedBranch, refSymbol, scopes)) return true;
|
|
36633
37082
|
}
|
|
36634
37083
|
descendant = ancestor;
|
|
36635
37084
|
ancestor = descendant.parent;
|
|
@@ -37203,7 +37652,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37203
37652
|
return expressionReadsStateValue(argument, stateName);
|
|
37204
37653
|
};
|
|
37205
37654
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37206
|
-
const expression =
|
|
37655
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
37207
37656
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37208
37657
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37209
37658
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -37552,7 +38001,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
37552
38001
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
37553
38002
|
for (const statement of functionBody.body ?? []) {
|
|
37554
38003
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37555
|
-
const effectCall = statement
|
|
38004
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
37556
38005
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
37557
38006
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
37558
38007
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -38182,9 +38631,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38182
38631
|
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.",
|
|
38183
38632
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38184
38633
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38185
|
-
const
|
|
38634
|
+
const elementName = resolveJsxElementType(node);
|
|
38635
|
+
const firstCharacter = elementName.charCodeAt(0);
|
|
38186
38636
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38187
|
-
if (
|
|
38637
|
+
if (elementName.includes("-")) return;
|
|
38188
38638
|
for (const attribute of node.attributes) {
|
|
38189
38639
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38190
38640
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38572,8 +39022,7 @@ const noUncontrolledInput = defineRule({
|
|
|
38572
39022
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38573
39023
|
walkAst(componentBody, (child) => {
|
|
38574
39024
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38575
|
-
|
|
38576
|
-
const tagName = child.name.name;
|
|
39025
|
+
const tagName = resolveJsxElementType(child);
|
|
38577
39026
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38578
39027
|
const attributes = child.attributes ?? [];
|
|
38579
39028
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38634,7 +39083,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38634
39083
|
severity: "warn",
|
|
38635
39084
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38636
39085
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38637
|
-
if (
|
|
39086
|
+
if (resolveJsxElementType(node) !== "script") return;
|
|
38638
39087
|
const attributes = node.attributes ?? [];
|
|
38639
39088
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38640
39089
|
if (!srcAttribute) return;
|
|
@@ -39849,7 +40298,7 @@ const noUnknownProperty = defineRule({
|
|
|
39849
40298
|
}
|
|
39850
40299
|
if (fileIsNonReactJsx) return;
|
|
39851
40300
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39852
|
-
const elementType = node
|
|
40301
|
+
const elementType = resolveJsxElementType(node);
|
|
39853
40302
|
const firstCharacter = elementType.charCodeAt(0);
|
|
39854
40303
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
39855
40304
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40027,20 +40476,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40027
40476
|
});
|
|
40028
40477
|
return found;
|
|
40029
40478
|
};
|
|
40479
|
+
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40030
40480
|
const isReactClassComponent = (classNode) => {
|
|
40031
40481
|
if (isEs6Component(classNode)) return true;
|
|
40032
40482
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40033
40483
|
};
|
|
40034
|
-
const findEnclosingComponent = (node) => {
|
|
40484
|
+
const findEnclosingComponent = (node, scopes) => {
|
|
40035
40485
|
let walker = node.parent;
|
|
40036
40486
|
while (walker) {
|
|
40037
40487
|
if (isFunctionLike$1(walker)) {
|
|
40038
40488
|
const componentName = inferFunctionLikeName(walker);
|
|
40039
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40489
|
+
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
|
|
40040
40490
|
component: walker,
|
|
40041
40491
|
name: componentName
|
|
40042
40492
|
};
|
|
40043
|
-
if (!componentName &&
|
|
40493
|
+
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40044
40494
|
component: walker,
|
|
40045
40495
|
name: null
|
|
40046
40496
|
};
|
|
@@ -40279,7 +40729,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40279
40729
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40280
40730
|
if (settings.allowAsProps) return;
|
|
40281
40731
|
}
|
|
40282
|
-
const enclosing = findEnclosingComponent(candidateNode);
|
|
40732
|
+
const enclosing = findEnclosingComponent(candidateNode, context.scopes);
|
|
40283
40733
|
if (!enclosing) return;
|
|
40284
40734
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40285
40735
|
queuedReports.push({
|
|
@@ -40290,7 +40740,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40290
40740
|
});
|
|
40291
40741
|
};
|
|
40292
40742
|
const checkFunctionLike = (node) => {
|
|
40293
|
-
if (!
|
|
40743
|
+
if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
|
|
40294
40744
|
const inferredName = inferFunctionLikeName(node);
|
|
40295
40745
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40296
40746
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41550,7 +42000,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41550
42000
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41551
42001
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41552
42002
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41553
|
-
const tagName = node
|
|
42003
|
+
const tagName = resolveJsxElementType(node);
|
|
41554
42004
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41555
42005
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41556
42006
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41570,7 +42020,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41570
42020
|
]);
|
|
41571
42021
|
const isTextLikeInput = (openingElement) => {
|
|
41572
42022
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41573
|
-
const tagName = openingElement
|
|
42023
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
41574
42024
|
if (tagName === "textarea") return true;
|
|
41575
42025
|
if (tagName !== "input") return false;
|
|
41576
42026
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41727,8 +42177,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41727
42177
|
]);
|
|
41728
42178
|
const collectBooleanPropBindings = (param) => {
|
|
41729
42179
|
const bindings = /* @__PURE__ */ new Set();
|
|
41730
|
-
|
|
41731
|
-
|
|
42180
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
42181
|
+
if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
|
|
42182
|
+
for (const property of propsBinding.properties ?? []) {
|
|
41732
42183
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41733
42184
|
if (property.computed) continue;
|
|
41734
42185
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -41989,7 +42440,7 @@ const preferHtmlDialog = defineRule({
|
|
|
41989
42440
|
},
|
|
41990
42441
|
JSXOpeningElement(node) {
|
|
41991
42442
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41992
|
-
const tagName = node
|
|
42443
|
+
const tagName = resolveJsxElementType(node);
|
|
41993
42444
|
if (tagName === "dialog") return;
|
|
41994
42445
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41995
42446
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43536,13 +43987,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43536
43987
|
}
|
|
43537
43988
|
return null;
|
|
43538
43989
|
};
|
|
43539
|
-
const resolveHookNameFromInitializer = (initializer) => {
|
|
43990
|
+
const resolveHookNameFromInitializer = (initializer, scopes) => {
|
|
43540
43991
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43541
43992
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43542
|
-
const
|
|
43543
|
-
if (
|
|
43544
|
-
if (!isNodeOfType(
|
|
43545
|
-
return resolveTanstackQueryHookName(
|
|
43993
|
+
const resolvedSymbol = resolveConstIdentifierAlias(initializer, scopes);
|
|
43994
|
+
if (resolvedSymbol?.kind !== "const" || !resolvedSymbol.initializer) return null;
|
|
43995
|
+
if (!isNodeOfType(resolvedSymbol.initializer, "CallExpression")) return null;
|
|
43996
|
+
return resolveTanstackQueryHookName(resolvedSymbol.initializer);
|
|
43546
43997
|
};
|
|
43547
43998
|
const queryNoRestDestructuring = defineRule({
|
|
43548
43999
|
id: "query-no-rest-destructuring",
|
|
@@ -43555,7 +44006,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43555
44006
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43556
44007
|
if (!node.init) return;
|
|
43557
44008
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43558
|
-
const hookName = resolveHookNameFromInitializer(node.init);
|
|
44009
|
+
const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
|
|
43559
44010
|
if (!hookName) return;
|
|
43560
44011
|
context.report({
|
|
43561
44012
|
node: node.id,
|
|
@@ -44026,7 +44477,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44026
44477
|
severity: "warn",
|
|
44027
44478
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44028
44479
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44029
|
-
if (
|
|
44480
|
+
if (resolveJsxElementType(node) !== "svg") return;
|
|
44030
44481
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44031
44482
|
node,
|
|
44032
44483
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -44910,7 +45361,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
|
|
|
44910
45361
|
"useParams",
|
|
44911
45362
|
"usePathname"
|
|
44912
45363
|
]);
|
|
44913
|
-
const findHookCallBindings = (componentBody) => {
|
|
45364
|
+
const findHookCallBindings = (componentBody, scopes) => {
|
|
44914
45365
|
const bindings = [];
|
|
44915
45366
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
44916
45367
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -44921,8 +45372,10 @@ const findHookCallBindings = (componentBody) => {
|
|
|
44921
45372
|
const callee = declarator.init.callee;
|
|
44922
45373
|
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
44923
45374
|
if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
|
|
45375
|
+
const valueSymbol = scopes.symbolFor(declarator.id);
|
|
45376
|
+
if (!valueSymbol) continue;
|
|
44924
45377
|
bindings.push({
|
|
44925
|
-
|
|
45378
|
+
valueSymbol,
|
|
44926
45379
|
hookName: callee.name,
|
|
44927
45380
|
declarator
|
|
44928
45381
|
});
|
|
@@ -44930,6 +45383,49 @@ const findHookCallBindings = (componentBody) => {
|
|
|
44930
45383
|
}
|
|
44931
45384
|
return bindings;
|
|
44932
45385
|
};
|
|
45386
|
+
const collectExactAliasSymbols = (componentBody, sourceSymbol, scopes) => {
|
|
45387
|
+
const symbols = [sourceSymbol];
|
|
45388
|
+
const symbolIds = new Set([sourceSymbol.id]);
|
|
45389
|
+
const aliasSourceIdentifiers = /* @__PURE__ */ new Set();
|
|
45390
|
+
if (!isNodeOfType(componentBody, "BlockStatement")) return {
|
|
45391
|
+
symbols,
|
|
45392
|
+
aliasSourceIdentifiers
|
|
45393
|
+
};
|
|
45394
|
+
let didFindAlias = true;
|
|
45395
|
+
while (didFindAlias) {
|
|
45396
|
+
didFindAlias = false;
|
|
45397
|
+
for (const statement of componentBody.body ?? []) {
|
|
45398
|
+
if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "const") continue;
|
|
45399
|
+
for (const declarator of statement.declarations ?? []) {
|
|
45400
|
+
let aliasIdentifier = null;
|
|
45401
|
+
let sourceIdentifier = null;
|
|
45402
|
+
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
45403
|
+
const arrayBinding = isNodeOfType(declarator.id, "ArrayPattern") ? declarator.id.elements[0] : null;
|
|
45404
|
+
const arrayValueNode = isNodeOfType(initializer, "ArrayExpression") ? initializer.elements[0] : null;
|
|
45405
|
+
const arrayValue = arrayValueNode && !isNodeOfType(arrayValueNode, "SpreadElement") ? stripParenExpression(arrayValueNode) : null;
|
|
45406
|
+
if (isNodeOfType(declarator.id, "Identifier") && isNodeOfType(initializer, "Identifier")) {
|
|
45407
|
+
aliasIdentifier = declarator.id;
|
|
45408
|
+
sourceIdentifier = initializer;
|
|
45409
|
+
} else if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements.length === 1 && isNodeOfType(arrayBinding, "Identifier") && isNodeOfType(initializer, "ArrayExpression") && initializer.elements.length === 1 && isNodeOfType(arrayValue, "Identifier")) {
|
|
45410
|
+
aliasIdentifier = arrayBinding;
|
|
45411
|
+
sourceIdentifier = arrayValue;
|
|
45412
|
+
}
|
|
45413
|
+
if (!aliasIdentifier || !sourceIdentifier) continue;
|
|
45414
|
+
const referencedSymbol = scopes.symbolFor(sourceIdentifier);
|
|
45415
|
+
const aliasSymbol = scopes.symbolFor(aliasIdentifier);
|
|
45416
|
+
if (!referencedSymbol || !symbolIds.has(referencedSymbol.id) || !aliasSymbol || aliasSymbol.kind !== "const" || symbolIds.has(aliasSymbol.id)) continue;
|
|
45417
|
+
symbolIds.add(aliasSymbol.id);
|
|
45418
|
+
symbols.push(aliasSymbol);
|
|
45419
|
+
aliasSourceIdentifiers.add(sourceIdentifier);
|
|
45420
|
+
didFindAlias = true;
|
|
45421
|
+
}
|
|
45422
|
+
}
|
|
45423
|
+
}
|
|
45424
|
+
return {
|
|
45425
|
+
symbols,
|
|
45426
|
+
aliasSourceIdentifiers
|
|
45427
|
+
};
|
|
45428
|
+
};
|
|
44933
45429
|
const rerenderDeferReadsHook = defineRule({
|
|
44934
45430
|
id: "rerender-defer-reads-hook",
|
|
44935
45431
|
title: "URL hook value only read in handlers",
|
|
@@ -44940,15 +45436,13 @@ const rerenderDeferReadsHook = defineRule({
|
|
|
44940
45436
|
create: (context) => {
|
|
44941
45437
|
const checkComponent = (componentBody) => {
|
|
44942
45438
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
44943
|
-
const bindings = findHookCallBindings(componentBody);
|
|
45439
|
+
const bindings = findHookCallBindings(componentBody, context.scopes);
|
|
44944
45440
|
if (bindings.length === 0) return;
|
|
44945
45441
|
const handlerBindingNames = collectHandlerBindingNames(componentBody);
|
|
44946
45442
|
for (const binding of bindings) {
|
|
45443
|
+
const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
|
|
44947
45444
|
const referenceLocations = [];
|
|
44948
|
-
|
|
44949
|
-
if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
|
|
44950
|
-
if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
|
|
44951
|
-
});
|
|
45445
|
+
for (const symbol of symbols) for (const reference of symbol.references) if (!aliasSourceIdentifiers.has(reference.identifier)) referenceLocations.push(reference.identifier);
|
|
44952
45446
|
if (referenceLocations.length === 0) continue;
|
|
44953
45447
|
if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
|
|
44954
45448
|
context.report({
|
|
@@ -45319,14 +45813,10 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45319
45813
|
//#endregion
|
|
45320
45814
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45321
45815
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45322
|
-
const callbackReturnsJsx = (callback) => {
|
|
45816
|
+
const callbackReturnsJsx = (callback, scopes) => {
|
|
45323
45817
|
if (!callback) return false;
|
|
45324
45818
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45325
|
-
|
|
45326
|
-
if (isJsxExpression(body)) return true;
|
|
45327
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45328
|
-
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45329
|
-
return false;
|
|
45819
|
+
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
|
|
45330
45820
|
};
|
|
45331
45821
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45332
45822
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45404,7 +45894,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45404
45894
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45405
45895
|
for (const declarator of stmt.declarations ?? []) {
|
|
45406
45896
|
const init = declarator.init;
|
|
45407
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45897
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
|
|
45408
45898
|
memoNode = declarator;
|
|
45409
45899
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45410
45900
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45470,8 +45960,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45470
45960
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45471
45961
|
const bindings = /* @__PURE__ */ new Map();
|
|
45472
45962
|
const params = functionNode.params ?? [];
|
|
45473
|
-
for (const param of params)
|
|
45474
|
-
|
|
45963
|
+
for (const [parameterIndex, param] of params.entries()) {
|
|
45964
|
+
const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
|
|
45965
|
+
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45966
|
+
}
|
|
45967
|
+
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45475
45968
|
const body = functionNode.body;
|
|
45476
45969
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45477
45970
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -45977,8 +46470,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
45977
46470
|
if (statements.length !== 1) return;
|
|
45978
46471
|
const onlyStatement = statements[0];
|
|
45979
46472
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
45980
|
-
singleAssignment = onlyStatement
|
|
45981
|
-
} else if (body) singleAssignment = body;
|
|
46473
|
+
singleAssignment = unwrapDiscardedExpression(onlyStatement);
|
|
46474
|
+
} else if (body) singleAssignment = unwrapDiscardedExpression(body);
|
|
45982
46475
|
if (!singleAssignment) return;
|
|
45983
46476
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
45984
46477
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|
|
@@ -52898,7 +53391,8 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
52898
53391
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
52899
53392
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
52900
53393
|
const firstArg = node.arguments?.[0];
|
|
52901
|
-
if (!isNodeOfType(firstArg, "
|
|
53394
|
+
if (!firstArg || isNodeOfType(firstArg, "SpreadElement")) return;
|
|
53395
|
+
if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
|
|
52902
53396
|
context.report({
|
|
52903
53397
|
node,
|
|
52904
53398
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|
|
@@ -53585,10 +54079,6 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53585
54079
|
}
|
|
53586
54080
|
return false;
|
|
53587
54081
|
};
|
|
53588
|
-
const getJsxOpeningElementName = (node) => {
|
|
53589
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53590
|
-
return null;
|
|
53591
|
-
};
|
|
53592
54082
|
const stylePropObject = defineRule({
|
|
53593
54083
|
id: "style-prop-object",
|
|
53594
54084
|
title: "Style prop is not an object",
|
|
@@ -53600,7 +54090,8 @@ const stylePropObject = defineRule({
|
|
|
53600
54090
|
const allowSet = new Set(allow);
|
|
53601
54091
|
return {
|
|
53602
54092
|
JSXOpeningElement(node) {
|
|
53603
|
-
|
|
54093
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
54094
|
+
const elementName = resolveJsxElementType(node);
|
|
53604
54095
|
if (elementName && allowSet.has(elementName)) return;
|
|
53605
54096
|
if (elementName) {
|
|
53606
54097
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54305,7 +54796,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54305
54796
|
create: (context) => {
|
|
54306
54797
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54307
54798
|
return { JSXOpeningElement(node) {
|
|
54308
|
-
if (
|
|
54799
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
54309
54800
|
const attributes = node.attributes ?? [];
|
|
54310
54801
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54311
54802
|
if (!hrefAttribute?.value) return;
|
|
@@ -54926,8 +55417,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
54926
55417
|
create: (context) => ({
|
|
54927
55418
|
JSXElement(node) {
|
|
54928
55419
|
const openingElement = node.openingElement;
|
|
54929
|
-
|
|
54930
|
-
const tagName = openingElement.name.name;
|
|
55420
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
54931
55421
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
54932
55422
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
54933
55423
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|