oxlint-plugin-react-doctor 0.7.6-dev.0a8d30b → 0.7.6-dev.17418
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 +226 -986
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1180,8 +1180,8 @@ const stripParenExpression = (node) => {
|
|
|
1180
1180
|
return current;
|
|
1181
1181
|
};
|
|
1182
1182
|
//#endregion
|
|
1183
|
-
//#region src/plugin/utils/get-root-identifier.ts
|
|
1184
|
-
const
|
|
1183
|
+
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1184
|
+
const getRootIdentifierName = (node, options) => {
|
|
1185
1185
|
if (!node) return null;
|
|
1186
1186
|
const followCallChains = options?.followCallChains === true;
|
|
1187
1187
|
let cursor = node;
|
|
@@ -1192,18 +1192,16 @@ const getRootIdentifier$1 = (node, options) => {
|
|
|
1192
1192
|
continue;
|
|
1193
1193
|
}
|
|
1194
1194
|
if (followCallChains && isNodeOfType(cursor, "CallExpression")) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1195
|
+
const callee = cursor.callee;
|
|
1196
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
1197
|
+
cursor = callee.object;
|
|
1197
1198
|
continue;
|
|
1198
1199
|
}
|
|
1199
1200
|
break;
|
|
1200
1201
|
}
|
|
1201
|
-
return isNodeOfType(cursor, "Identifier") ? cursor : null;
|
|
1202
|
+
return isNodeOfType(cursor, "Identifier") ? cursor.name : null;
|
|
1202
1203
|
};
|
|
1203
1204
|
//#endregion
|
|
1204
|
-
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1205
|
-
const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
|
|
1206
|
-
//#endregion
|
|
1207
1205
|
//#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
|
|
1208
1206
|
const STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
1209
1207
|
"useCallback",
|
|
@@ -1520,37 +1518,6 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1520
1518
|
}
|
|
1521
1519
|
};
|
|
1522
1520
|
//#endregion
|
|
1523
|
-
//#region src/plugin/utils/is-uppercase-name.ts
|
|
1524
|
-
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
1525
|
-
//#endregion
|
|
1526
|
-
//#region src/plugin/utils/resolve-jsx-element-type.ts
|
|
1527
|
-
const resolveConstantStringBinding = (name) => {
|
|
1528
|
-
const binding = findVariableInitializer(name, name.name);
|
|
1529
|
-
if (!binding?.initializer) return null;
|
|
1530
|
-
const declarator = binding.bindingIdentifier.parent;
|
|
1531
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
1532
|
-
if (declarator.id !== binding.bindingIdentifier) return null;
|
|
1533
|
-
const declaration = declarator.parent;
|
|
1534
|
-
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
1535
|
-
if (declaration.kind !== "const") return null;
|
|
1536
|
-
const initializer = stripParenExpression(binding.initializer);
|
|
1537
|
-
return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
1538
|
-
};
|
|
1539
|
-
const flattenJsxName$2 = (name) => {
|
|
1540
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1541
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1542
|
-
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1543
|
-
return "";
|
|
1544
|
-
};
|
|
1545
|
-
const resolveJsxElementType = (openingElement) => {
|
|
1546
|
-
const name = openingElement.name;
|
|
1547
|
-
if (isNodeOfType(name, "JSXIdentifier")) {
|
|
1548
|
-
if (!isUppercaseName(name.name)) return name.name;
|
|
1549
|
-
return resolveConstantStringBinding(name) ?? name.name;
|
|
1550
|
-
}
|
|
1551
|
-
return flattenJsxName$2(name);
|
|
1552
|
-
};
|
|
1553
|
-
//#endregion
|
|
1554
1521
|
//#region src/plugin/utils/get-element-type.ts
|
|
1555
1522
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1556
1523
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1563,8 +1530,14 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1563
1530
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1564
1531
|
return a11ySettings;
|
|
1565
1532
|
};
|
|
1533
|
+
const flattenJsxName$2 = (name) => {
|
|
1534
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1535
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1536
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1537
|
+
return "";
|
|
1538
|
+
};
|
|
1566
1539
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1567
|
-
const baseName =
|
|
1540
|
+
const baseName = flattenJsxName$2(openingElement.name);
|
|
1568
1541
|
if (a11ySettings.polymorphicPropName) {
|
|
1569
1542
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1570
1543
|
if (polymorphicAttribute) {
|
|
@@ -2399,9 +2372,8 @@ const anchorIsValid = defineRule({
|
|
|
2399
2372
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2400
2373
|
return { JSXOpeningElement(node) {
|
|
2401
2374
|
if (isTestlikeFile) return;
|
|
2402
|
-
|
|
2403
|
-
if (
|
|
2404
|
-
if (tag !== "a") return;
|
|
2375
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2376
|
+
if (getElementType(node, context.settings) !== "a") return;
|
|
2405
2377
|
let hrefAttribute;
|
|
2406
2378
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2407
2379
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5756,7 +5728,7 @@ const buttonHasType = defineRule({
|
|
|
5756
5728
|
return {
|
|
5757
5729
|
JSXOpeningElement(node) {
|
|
5758
5730
|
if (isTestlikeFile) return;
|
|
5759
|
-
if (
|
|
5731
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "button") return;
|
|
5760
5732
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5761
5733
|
if (!typeAttr) {
|
|
5762
5734
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5946,7 +5918,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5946
5918
|
};
|
|
5947
5919
|
return {
|
|
5948
5920
|
JSXOpeningElement(node) {
|
|
5949
|
-
if (
|
|
5921
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
5950
5922
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5951
5923
|
},
|
|
5952
5924
|
CallExpression(node) {
|
|
@@ -5964,7 +5936,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5964
5936
|
//#endregion
|
|
5965
5937
|
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
5966
5938
|
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
5967
|
-
if (!isNodeOfType(node, "Property")
|
|
5939
|
+
if (!isNodeOfType(node, "Property")) return null;
|
|
5968
5940
|
if (node.computed) {
|
|
5969
5941
|
if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
|
|
5970
5942
|
return null;
|
|
@@ -6023,7 +5995,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
6023
5995
|
//#endregion
|
|
6024
5996
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
6025
5997
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
6026
|
-
if (!isNodeOfType(identifier, "Identifier")
|
|
5998
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6027
5999
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6028
6000
|
let symbol = scopes.symbolFor(identifier);
|
|
6029
6001
|
while (symbol?.kind === "const") {
|
|
@@ -6298,131 +6270,6 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6298
6270
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6299
6271
|
};
|
|
6300
6272
|
//#endregion
|
|
6301
|
-
//#region src/plugin/utils/collect-safely-validated-local-storage-keys.ts
|
|
6302
|
-
const isLocalStorageCall = (node, methodName) => {
|
|
6303
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6304
|
-
const callee = stripParenExpression(node.callee);
|
|
6305
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
6306
|
-
const receiver = stripParenExpression(callee.object);
|
|
6307
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "localStorage" && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6308
|
-
};
|
|
6309
|
-
const catchReturnsFallback = (tryStatement) => {
|
|
6310
|
-
const handler = tryStatement.handler;
|
|
6311
|
-
if (!handler) return false;
|
|
6312
|
-
let hasReturn = false;
|
|
6313
|
-
let hasThrow = false;
|
|
6314
|
-
walkAst(handler.body, (child) => {
|
|
6315
|
-
if (isFunctionLike$1(child)) return false;
|
|
6316
|
-
if (isNodeOfType(child, "ReturnStatement")) hasReturn = true;
|
|
6317
|
-
if (isNodeOfType(child, "ThrowStatement")) hasThrow = true;
|
|
6318
|
-
});
|
|
6319
|
-
return hasReturn && !hasThrow;
|
|
6320
|
-
};
|
|
6321
|
-
const resolveValidatorFunction = (callee, scopes) => {
|
|
6322
|
-
const unwrappedCallee = stripParenExpression(callee);
|
|
6323
|
-
if (!isNodeOfType(unwrappedCallee, "Identifier")) return null;
|
|
6324
|
-
const symbol = scopes.symbolFor(unwrappedCallee);
|
|
6325
|
-
if (!symbol) return null;
|
|
6326
|
-
const candidate = symbol.kind === "function" ? symbol.declarationNode : symbol.initializer;
|
|
6327
|
-
return isFunctionLike$1(candidate) ? candidate : null;
|
|
6328
|
-
};
|
|
6329
|
-
const validatorChecksPayloadProperties = (validatorFunction, scopes) => {
|
|
6330
|
-
if (!isFunctionLike$1(validatorFunction)) return false;
|
|
6331
|
-
const firstParameter = validatorFunction.params?.[0];
|
|
6332
|
-
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
6333
|
-
const parameterSymbol = scopes.symbolFor(firstParameter);
|
|
6334
|
-
if (!parameterSymbol) return false;
|
|
6335
|
-
const payloadSymbolIds = new Set([parameterSymbol.id]);
|
|
6336
|
-
let hasPropertyTypeCheck = false;
|
|
6337
|
-
walkAst(validatorFunction.body, (child) => {
|
|
6338
|
-
if (isFunctionLike$1(child)) return false;
|
|
6339
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.init) {
|
|
6340
|
-
const initializer = stripParenExpression(child.init);
|
|
6341
|
-
if (isNodeOfType(initializer, "Identifier")) {
|
|
6342
|
-
const initializerSymbol = scopes.symbolFor(initializer);
|
|
6343
|
-
if (initializerSymbol && payloadSymbolIds.has(initializerSymbol.id)) {
|
|
6344
|
-
const aliasSymbol = scopes.symbolFor(child.id);
|
|
6345
|
-
if (aliasSymbol) payloadSymbolIds.add(aliasSymbol.id);
|
|
6346
|
-
}
|
|
6347
|
-
}
|
|
6348
|
-
}
|
|
6349
|
-
if (!isNodeOfType(child, "UnaryExpression") || child.operator !== "typeof") return;
|
|
6350
|
-
const checkedValue = stripParenExpression(child.argument);
|
|
6351
|
-
if (!isNodeOfType(checkedValue, "MemberExpression")) return;
|
|
6352
|
-
const receiver = stripParenExpression(checkedValue.object);
|
|
6353
|
-
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6354
|
-
const receiverSymbol = scopes.symbolFor(receiver);
|
|
6355
|
-
if (receiverSymbol && payloadSymbolIds.has(receiverSymbol.id)) hasPropertyTypeCheck = true;
|
|
6356
|
-
});
|
|
6357
|
-
return hasPropertyTypeCheck;
|
|
6358
|
-
};
|
|
6359
|
-
const incrementCount = (counts, keyValue) => {
|
|
6360
|
-
counts.set(keyValue, (counts.get(keyValue) ?? 0) + 1);
|
|
6361
|
-
};
|
|
6362
|
-
const isReturnedExpression = (expression) => {
|
|
6363
|
-
const parent = expression.parent;
|
|
6364
|
-
return Boolean(parent && isNodeOfType(parent, "ReturnStatement") && parent.argument === expression);
|
|
6365
|
-
};
|
|
6366
|
-
const collectSafelyValidatedLocalStorageKeys = ({ programNode, scopes, resolveKey }) => {
|
|
6367
|
-
const readCountsByKey = /* @__PURE__ */ new Map();
|
|
6368
|
-
const safeReadCountsByKey = /* @__PURE__ */ new Map();
|
|
6369
|
-
const safeReadSymbolIds = /* @__PURE__ */ new Set();
|
|
6370
|
-
walkAst(programNode, (child) => {
|
|
6371
|
-
if (!isNodeOfType(child, "CallExpression") || !isLocalStorageCall(child, "getItem")) return;
|
|
6372
|
-
const keyArgument = child.arguments?.[0];
|
|
6373
|
-
if (!keyArgument) return;
|
|
6374
|
-
const keyValue = resolveKey(keyArgument);
|
|
6375
|
-
if (keyValue !== null) incrementCount(readCountsByKey, keyValue);
|
|
6376
|
-
});
|
|
6377
|
-
walkAst(programNode, (child) => {
|
|
6378
|
-
if (!isNodeOfType(child, "TryStatement") || !catchReturnsFallback(child)) return;
|
|
6379
|
-
const rawValueKeys = /* @__PURE__ */ new Map();
|
|
6380
|
-
const parsedValueSources = /* @__PURE__ */ new Map();
|
|
6381
|
-
walkAst(child.block, (tryChild) => {
|
|
6382
|
-
if (isFunctionLike$1(tryChild)) return false;
|
|
6383
|
-
if (isNodeOfType(tryChild, "VariableDeclarator") && isNodeOfType(tryChild.id, "Identifier") && tryChild.init) {
|
|
6384
|
-
const initializer = stripParenExpression(tryChild.init);
|
|
6385
|
-
if (isNodeOfType(initializer, "CallExpression") && isLocalStorageCall(initializer, "getItem")) {
|
|
6386
|
-
const keyArgument = initializer.arguments?.[0];
|
|
6387
|
-
const bindingSymbol = scopes.symbolFor(tryChild.id);
|
|
6388
|
-
if (keyArgument && bindingSymbol) {
|
|
6389
|
-
const keyValue = resolveKey(keyArgument);
|
|
6390
|
-
if (keyValue !== null) rawValueKeys.set(bindingSymbol.id, keyValue);
|
|
6391
|
-
}
|
|
6392
|
-
}
|
|
6393
|
-
if (isNodeOfType(initializer, "CallExpression") && isGlobalMethodCall(initializer, "JSON", "parse")) {
|
|
6394
|
-
const rawValueArgument = initializer.arguments?.[0];
|
|
6395
|
-
const parsedValueSymbol = scopes.symbolFor(tryChild.id);
|
|
6396
|
-
if (rawValueArgument && isNodeOfType(rawValueArgument, "Identifier") && parsedValueSymbol) {
|
|
6397
|
-
const rawValueSymbol = scopes.symbolFor(rawValueArgument);
|
|
6398
|
-
if (rawValueSymbol && rawValueKeys.has(rawValueSymbol.id)) parsedValueSources.set(parsedValueSymbol.id, rawValueSymbol.id);
|
|
6399
|
-
}
|
|
6400
|
-
}
|
|
6401
|
-
}
|
|
6402
|
-
if (!isNodeOfType(tryChild, "ConditionalExpression") || !isReturnedExpression(tryChild)) return;
|
|
6403
|
-
const test = stripParenExpression(tryChild.test);
|
|
6404
|
-
if (!isNodeOfType(test, "CallExpression")) return;
|
|
6405
|
-
const testedValue = test.arguments?.[0];
|
|
6406
|
-
if (!testedValue || !isNodeOfType(testedValue, "Identifier")) return;
|
|
6407
|
-
const parsedValueSymbol = scopes.symbolFor(testedValue);
|
|
6408
|
-
if (!parsedValueSymbol) return;
|
|
6409
|
-
const rawValueSymbolId = parsedValueSources.get(parsedValueSymbol.id);
|
|
6410
|
-
if (rawValueSymbolId === void 0) return;
|
|
6411
|
-
const returnedValue = stripParenExpression(tryChild.consequent);
|
|
6412
|
-
if (!isNodeOfType(returnedValue, "Identifier") || scopes.symbolFor(returnedValue)?.id !== parsedValueSymbol.id) return;
|
|
6413
|
-
const validatorFunction = resolveValidatorFunction(test.callee, scopes);
|
|
6414
|
-
if (!validatorFunction || !validatorChecksPayloadProperties(validatorFunction, scopes)) return;
|
|
6415
|
-
const keyValue = rawValueKeys.get(rawValueSymbolId);
|
|
6416
|
-
if (keyValue === void 0 || safeReadSymbolIds.has(rawValueSymbolId)) return;
|
|
6417
|
-
safeReadSymbolIds.add(rawValueSymbolId);
|
|
6418
|
-
incrementCount(safeReadCountsByKey, keyValue);
|
|
6419
|
-
});
|
|
6420
|
-
});
|
|
6421
|
-
const safeKeys = /* @__PURE__ */ new Set();
|
|
6422
|
-
for (const [keyValue, readCount] of readCountsByKey) if (safeReadCountsByKey.get(keyValue) === readCount) safeKeys.add(keyValue);
|
|
6423
|
-
return safeKeys;
|
|
6424
|
-
};
|
|
6425
|
-
//#endregion
|
|
6426
6273
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6427
6274
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6428
6275
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6444,39 +6291,26 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6444
6291
|
severity: "warn",
|
|
6445
6292
|
category: "Correctness",
|
|
6446
6293
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6447
|
-
create: (context) => {
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
if (keyValue === null) return;
|
|
6468
|
-
if (isVersionedKey(keyValue)) return;
|
|
6469
|
-
if (safelyValidatedStorageKeys.has(keyValue)) return;
|
|
6470
|
-
const valueArg = node.arguments?.[1];
|
|
6471
|
-
if (!valueArg) return;
|
|
6472
|
-
if (!isJsonStringifyCall(valueArg)) return;
|
|
6473
|
-
context.report({
|
|
6474
|
-
node: keyArg,
|
|
6475
|
-
message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6476
|
-
});
|
|
6477
|
-
}
|
|
6478
|
-
};
|
|
6479
|
-
}
|
|
6294
|
+
create: (context) => ({ CallExpression(node) {
|
|
6295
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6296
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6297
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6298
|
+
if (receiver.name !== "localStorage") return;
|
|
6299
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6300
|
+
if (node.callee.property.name !== "setItem") return;
|
|
6301
|
+
const keyArg = node.arguments?.[0];
|
|
6302
|
+
if (!keyArg) return;
|
|
6303
|
+
const keyValue = resolveStringKey(keyArg, context);
|
|
6304
|
+
if (keyValue === null) return;
|
|
6305
|
+
if (isVersionedKey(keyValue)) return;
|
|
6306
|
+
const valueArg = node.arguments?.[1];
|
|
6307
|
+
if (!valueArg) return;
|
|
6308
|
+
if (!isJsonStringifyCall(valueArg)) return;
|
|
6309
|
+
context.report({
|
|
6310
|
+
node: keyArg,
|
|
6311
|
+
message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6312
|
+
});
|
|
6313
|
+
} })
|
|
6480
6314
|
});
|
|
6481
6315
|
//#endregion
|
|
6482
6316
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -7312,17 +7146,12 @@ const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) =>
|
|
|
7312
7146
|
}
|
|
7313
7147
|
return nearestDeclaration;
|
|
7314
7148
|
};
|
|
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
|
-
};
|
|
7320
7149
|
const isTrustedHighlighterValue = (valueExpression, fileContent, sinkIndex) => {
|
|
7321
7150
|
if (!/highlight/i.test(valueExpression)) return false;
|
|
7322
7151
|
const identifier = valueExpression.match(/^([\w$]+)/)?.[1];
|
|
7323
7152
|
if (identifier === void 0) return false;
|
|
7324
7153
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7325
|
-
if (declaration !== null) return
|
|
7154
|
+
if (declaration !== null) return SERIALIZER_CALL_PROVENANCE_PATTERN.test(declaration.initializer);
|
|
7326
7155
|
return /highlighted/i.test(valueExpression) || HIGHLIGHTER_LIBRARY_PATTERN.test(fileContent);
|
|
7327
7156
|
};
|
|
7328
7157
|
const isExplicitlyTrustedHtmlValue = (valueExpression, fileContent, sinkIndex, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
@@ -7334,7 +7163,7 @@ const isExplicitlyTrustedHtmlValue = (valueExpression, fileContent, sinkIndex, v
|
|
|
7334
7163
|
const identifier = trimmedExpression.match(/^([\w$]+)(?:(?:\??\.[\w$]+)|(?:\[\s*(?:"[^"\n]*"|'[^'\n]*'|\d+)\s*\]))*$/)?.[1];
|
|
7335
7164
|
if (identifier && !visitedIdentifiers.has(identifier)) {
|
|
7336
7165
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7337
|
-
if (declaration
|
|
7166
|
+
if (declaration) {
|
|
7338
7167
|
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7339
7168
|
nextVisitedIdentifiers.add(identifier);
|
|
7340
7169
|
return isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex, nextVisitedIdentifiers);
|
|
@@ -7401,16 +7230,6 @@ const splitTopLevelArguments = (argumentText) => {
|
|
|
7401
7230
|
argumentsList.push(argumentText.slice(startIndex).trim());
|
|
7402
7231
|
return argumentsList;
|
|
7403
7232
|
};
|
|
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
|
-
};
|
|
7414
7233
|
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers, visitedCallSites) => {
|
|
7415
7234
|
const trimmedExpression = expression.trim();
|
|
7416
7235
|
if (isExplicitlyTrustedHtmlValue(trimmedExpression, fileContent, sinkIndex)) return false;
|
|
@@ -7421,11 +7240,8 @@ const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers, v
|
|
|
7421
7240
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7422
7241
|
const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
|
|
7423
7242
|
if (declaration !== null && (parameterSource === null || declaration.declarationIndex > parameterSource.declarationNameIndex)) {
|
|
7424
|
-
if (
|
|
7425
|
-
|
|
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);
|
|
7243
|
+
if (isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex)) return false;
|
|
7244
|
+
return isHtmlTainted(declaration.initializer, fileContent, declaration.initializerStartIndex, visitedIdentifiers, visitedCallSites) || HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7429
7245
|
}
|
|
7430
7246
|
if (parameterSource !== null && parameterSource.functionName.length > 0) {
|
|
7431
7247
|
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(`, "g");
|
|
@@ -7507,13 +7323,12 @@ const dangerousHtmlSink = defineRule({
|
|
|
7507
7323
|
if (isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
|
|
7508
7324
|
if (valueIdentifier !== void 0) {
|
|
7509
7325
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
7510
|
-
const
|
|
7511
|
-
const visibleInitializer = visibleDeclaration?.initializer;
|
|
7326
|
+
const visibleInitializer = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content)?.initializer;
|
|
7512
7327
|
if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
|
|
7513
7328
|
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7514
|
-
if (visibleInitializer === void 0 ? fromSerializer.test(file.content) :
|
|
7329
|
+
if (visibleInitializer === void 0 ? fromSerializer.test(file.content) : SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer)) continue;
|
|
7515
7330
|
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7516
|
-
if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) :
|
|
7331
|
+
if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`)) continue;
|
|
7517
7332
|
}
|
|
7518
7333
|
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
7519
7334
|
if (new RegExp(`highlight[\\w$]*\\s*\\.map\\(\\s*(?:async\\s+)?\\(?\\s*${escapedIdentifier}\\b`, "i").test(file.content) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
@@ -7599,7 +7414,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7599
7414
|
const getOpeningElementTagName = (openingElement) => {
|
|
7600
7415
|
if (!openingElement) return null;
|
|
7601
7416
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7602
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return
|
|
7417
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement.name.name;
|
|
7603
7418
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7604
7419
|
let cursor = openingElement.name;
|
|
7605
7420
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7851,7 +7666,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7851
7666
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7852
7667
|
return { JSXOpeningElement(node) {
|
|
7853
7668
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7854
|
-
const tagName =
|
|
7669
|
+
const tagName = node.name.name;
|
|
7855
7670
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7856
7671
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7857
7672
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -12083,7 +11898,7 @@ const forbidDomProps = defineRule({
|
|
|
12083
11898
|
return { JSXOpeningElement(node) {
|
|
12084
11899
|
if (forbidMap.size === 0) return;
|
|
12085
11900
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12086
|
-
const elementName =
|
|
11901
|
+
const elementName = node.name.name;
|
|
12087
11902
|
if (isReactComponentName(elementName)) return;
|
|
12088
11903
|
for (const attribute of node.attributes) {
|
|
12089
11904
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -12161,7 +11976,7 @@ const forbidElements = defineRule({
|
|
|
12161
11976
|
return {
|
|
12162
11977
|
JSXOpeningElement(node) {
|
|
12163
11978
|
if (forbidMap.size === 0) return;
|
|
12164
|
-
const fullName =
|
|
11979
|
+
const fullName = flattenJsxName$1(node.name);
|
|
12165
11980
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
12166
11981
|
context.report({
|
|
12167
11982
|
node: node.name,
|
|
@@ -12523,7 +12338,8 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12523
12338
|
const isParagraphElement = (candidate) => {
|
|
12524
12339
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12525
12340
|
const opening = candidate.openingElement;
|
|
12526
|
-
|
|
12341
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12342
|
+
return opening.name.name === "p";
|
|
12527
12343
|
};
|
|
12528
12344
|
const findEnclosingParagraph = (openingElement) => {
|
|
12529
12345
|
const owningElement = openingElement.parent;
|
|
@@ -12544,7 +12360,8 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12544
12360
|
severity: "warn",
|
|
12545
12361
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12546
12362
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12547
|
-
|
|
12363
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12364
|
+
const childTagName = node.name.name;
|
|
12548
12365
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12549
12366
|
if (!findEnclosingParagraph(node)) return;
|
|
12550
12367
|
context.report({
|
|
@@ -12575,7 +12392,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12575
12392
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12576
12393
|
const opening = jsxElement.openingElement;
|
|
12577
12394
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12578
|
-
const tagName =
|
|
12395
|
+
const tagName = opening.name.name;
|
|
12579
12396
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12580
12397
|
return tagName;
|
|
12581
12398
|
};
|
|
@@ -12605,7 +12422,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12605
12422
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12606
12423
|
const opening = ancestor.openingElement;
|
|
12607
12424
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12608
|
-
const ancestorTag =
|
|
12425
|
+
const ancestorTag = opening.name.name;
|
|
12609
12426
|
if (ancestorTag.length === 0) {
|
|
12610
12427
|
previous = ancestor;
|
|
12611
12428
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12687,7 +12504,8 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12687
12504
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12688
12505
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12689
12506
|
const opening = candidate.openingElement;
|
|
12690
|
-
|
|
12507
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12508
|
+
return opening.name.name === tagName;
|
|
12691
12509
|
};
|
|
12692
12510
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12693
12511
|
const owningElement = openingElement.parent;
|
|
@@ -12708,7 +12526,8 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12708
12526
|
severity: "warn",
|
|
12709
12527
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12710
12528
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12711
|
-
|
|
12529
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12530
|
+
const tagName = node.name.name;
|
|
12712
12531
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12713
12532
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12714
12533
|
context.report({
|
|
@@ -12823,7 +12642,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12823
12642
|
matchByOccurrence: true,
|
|
12824
12643
|
create: skipNonProductionFiles((context) => ({
|
|
12825
12644
|
JSXOpeningElement(node) {
|
|
12826
|
-
if (
|
|
12645
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
|
|
12827
12646
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12828
12647
|
if (!sandboxAttr) {
|
|
12829
12648
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -13815,28 +13634,6 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13815
13634
|
} })
|
|
13816
13635
|
});
|
|
13817
13636
|
//#endregion
|
|
13818
|
-
//#region src/plugin/utils/unwrap-discarded-expression.ts
|
|
13819
|
-
const unwrapDiscardedExpression = (node) => {
|
|
13820
|
-
let expression = isNodeOfType(node, "ExpressionStatement") ? node.expression : node;
|
|
13821
|
-
for (;;) {
|
|
13822
|
-
expression = stripParenExpression(expression);
|
|
13823
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") {
|
|
13824
|
-
expression = expression.argument;
|
|
13825
|
-
continue;
|
|
13826
|
-
}
|
|
13827
|
-
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
13828
|
-
const expressions = expression.expressions ?? [];
|
|
13829
|
-
const finalExpression = expressions.at(-1);
|
|
13830
|
-
const prefixExpressions = expressions.slice(0, -1);
|
|
13831
|
-
if (finalExpression && prefixExpressions.length > 0 && prefixExpressions.every((prefixExpression) => isNodeOfType(stripParenExpression(prefixExpression), "Literal"))) {
|
|
13832
|
-
expression = finalExpression;
|
|
13833
|
-
continue;
|
|
13834
|
-
}
|
|
13835
|
-
}
|
|
13836
|
-
return expression;
|
|
13837
|
-
}
|
|
13838
|
-
};
|
|
13839
|
-
//#endregion
|
|
13840
13637
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13841
13638
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13842
13639
|
"forEach",
|
|
@@ -14021,19 +13818,9 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
14021
13818
|
});
|
|
14022
13819
|
return foundAttachment;
|
|
14023
13820
|
};
|
|
14024
|
-
const getStyleAssignment = (node) => {
|
|
14025
|
-
if (!isNodeOfType(node, "ExpressionStatement")) return null;
|
|
14026
|
-
const expression = unwrapDiscardedExpression(node);
|
|
14027
|
-
if (!isNodeOfType(expression, "AssignmentExpression")) return null;
|
|
14028
|
-
if (!isNodeOfType(expression.left, "MemberExpression")) return null;
|
|
14029
|
-
if (!isNodeOfType(expression.left.object, "MemberExpression")) return null;
|
|
14030
|
-
if (!isNodeOfType(expression.left.object.property, "Identifier")) return null;
|
|
14031
|
-
return expression.left.object.property.name === "style" ? expression : null;
|
|
14032
|
-
};
|
|
14033
13821
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
const elementExpression = assignment.left.object.object;
|
|
13822
|
+
if (!isNodeOfType(styleWriteStatement, "ExpressionStatement") || !isNodeOfType(styleWriteStatement.expression, "AssignmentExpression") || !isNodeOfType(styleWriteStatement.expression.left, "MemberExpression") || !isNodeOfType(styleWriteStatement.expression.left.object, "MemberExpression")) return false;
|
|
13823
|
+
const elementExpression = styleWriteStatement.expression.left.object.object;
|
|
14037
13824
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
14038
13825
|
if (!creationRoot) return false;
|
|
14039
13826
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -14045,17 +13832,15 @@ const jsBatchDomCss = defineRule({
|
|
|
14045
13832
|
severity: "warn",
|
|
14046
13833
|
recommendation: "Do all your reads first, then all your writes. Mixing them inside a loop makes the browser recalculate the layout again and again, which is slow",
|
|
14047
13834
|
create: (context) => {
|
|
14048
|
-
const
|
|
14049
|
-
|
|
14050
|
-
return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
|
|
14051
|
-
};
|
|
13835
|
+
const isStyleAssignment = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.object, "MemberExpression") && isNodeOfType(node.expression.left.object.property, "Identifier") && node.expression.left.object.property.name === "style";
|
|
13836
|
+
const writesLayoutAffectingProperty = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(node.expression.left.property.name);
|
|
14052
13837
|
return { BlockStatement(node) {
|
|
14053
13838
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
14054
13839
|
if (!perIterationBody) return;
|
|
14055
13840
|
const statements = node.body ?? [];
|
|
14056
13841
|
let layoutReads = null;
|
|
14057
13842
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
14058
|
-
if (
|
|
13843
|
+
if (!isStyleAssignment(statements[statementIndex]) || !isStyleAssignment(statements[statementIndex - 1])) continue;
|
|
14059
13844
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
14060
13845
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
14061
13846
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -14751,18 +14536,6 @@ const jsHoistRegexp = defineRule({
|
|
|
14751
14536
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14752
14537
|
});
|
|
14753
14538
|
//#endregion
|
|
14754
|
-
//#region src/plugin/utils/resolve-first-argument-binding.ts
|
|
14755
|
-
const resolveFirstArgumentBinding = (firstParameter) => {
|
|
14756
|
-
if (!firstParameter) return null;
|
|
14757
|
-
if (!isNodeOfType(firstParameter, "RestElement")) return firstParameter;
|
|
14758
|
-
if (!isNodeOfType(firstParameter.argument, "ArrayPattern")) return null;
|
|
14759
|
-
const elements = firstParameter.argument.elements ?? [];
|
|
14760
|
-
if (elements.length !== 1) return null;
|
|
14761
|
-
const firstBinding = elements[0];
|
|
14762
|
-
if (!firstBinding || isNodeOfType(firstBinding, "RestElement")) return null;
|
|
14763
|
-
return firstBinding;
|
|
14764
|
-
};
|
|
14765
|
-
//#endregion
|
|
14766
14539
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14767
14540
|
const referencesParameter = (expression, parameterName) => {
|
|
14768
14541
|
if (!expression) return false;
|
|
@@ -14773,7 +14546,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14773
14546
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14774
14547
|
const callback = node.arguments?.[0];
|
|
14775
14548
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14776
|
-
const firstParameter =
|
|
14549
|
+
const firstParameter = callback.params?.[0];
|
|
14777
14550
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14778
14551
|
let predicate = null;
|
|
14779
14552
|
const body = callback.body;
|
|
@@ -15465,21 +15238,9 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15465
15238
|
};
|
|
15466
15239
|
const getResolvedInitializer = (receiver) => {
|
|
15467
15240
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15468
|
-
const
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
|
|
15472
|
-
if (isNodeOfType(initializer, "Identifier")) {
|
|
15473
|
-
const aliased = findVariableInitializer(initializer, initializer.name);
|
|
15474
|
-
if (aliased?.initializer) return {
|
|
15475
|
-
initializer: aliased.initializer,
|
|
15476
|
-
isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
|
|
15477
|
-
};
|
|
15478
|
-
}
|
|
15479
|
-
return {
|
|
15480
|
-
initializer,
|
|
15481
|
-
isDefault
|
|
15482
|
-
};
|
|
15241
|
+
const initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
|
|
15242
|
+
if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
|
|
15243
|
+
return initializer;
|
|
15483
15244
|
};
|
|
15484
15245
|
const isSplitCall = (expression) => {
|
|
15485
15246
|
if (!expression) return false;
|
|
@@ -15645,8 +15406,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15645
15406
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15646
15407
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15647
15408
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15648
|
-
const
|
|
15649
|
-
if (
|
|
15409
|
+
const initializer = getResolvedInitializer(stripped);
|
|
15410
|
+
if (initializer && isSmallInlineLiteralArray(initializer)) return true;
|
|
15650
15411
|
}
|
|
15651
15412
|
return false;
|
|
15652
15413
|
};
|
|
@@ -15698,8 +15459,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15698
15459
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15699
15460
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15700
15461
|
if (resolvedInitializer) {
|
|
15701
|
-
if (isLikelyStringReceiver(resolvedInitializer
|
|
15702
|
-
if (
|
|
15462
|
+
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15463
|
+
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15703
15464
|
}
|
|
15704
15465
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15705
15466
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -16112,21 +15873,14 @@ const jsxFilenameExtension = defineRule({
|
|
|
16112
15873
|
});
|
|
16113
15874
|
//#endregion
|
|
16114
15875
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
16115
|
-
const isJsxFragmentElement = (node
|
|
15876
|
+
const isJsxFragmentElement = (node) => {
|
|
16116
15877
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
16117
15878
|
const elementName = node.name;
|
|
16118
|
-
if (isNodeOfType(elementName, "JSXIdentifier"))
|
|
16119
|
-
if (!scopes) return elementName.name === "Fragment";
|
|
16120
|
-
const symbol = resolveConstIdentifierAlias(elementName, scopes);
|
|
16121
|
-
if (!symbol) return elementName.name === "Fragment" && scopes.isGlobalReference(elementName);
|
|
16122
|
-
return isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "Fragment";
|
|
16123
|
-
}
|
|
15879
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) return elementName.name === "Fragment";
|
|
16124
15880
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
16125
15881
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
16126
|
-
if (elementName.
|
|
16127
|
-
|
|
16128
|
-
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
16129
|
-
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15882
|
+
if (elementName.object.name !== "React") return false;
|
|
15883
|
+
return elementName.property.name === "Fragment";
|
|
16130
15884
|
}
|
|
16131
15885
|
return false;
|
|
16132
15886
|
};
|
|
@@ -16152,7 +15906,7 @@ const jsxFragments = defineRule({
|
|
|
16152
15906
|
if (mode !== "syntax") return;
|
|
16153
15907
|
if (!node.closingElement) return;
|
|
16154
15908
|
const openingElement = node.openingElement;
|
|
16155
|
-
if (!isJsxFragmentElement(openingElement
|
|
15909
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
16156
15910
|
if (openingElement.attributes.length > 0) return;
|
|
16157
15911
|
context.report({
|
|
16158
15912
|
node: openingElement,
|
|
@@ -18833,6 +18587,10 @@ const resolveSettings$29 = (settings) => {
|
|
|
18833
18587
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18834
18588
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18835
18589
|
};
|
|
18590
|
+
const getElementName = (node) => {
|
|
18591
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18592
|
+
return null;
|
|
18593
|
+
};
|
|
18836
18594
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18837
18595
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18838
18596
|
const explicit = options.components?.[elementName];
|
|
@@ -18852,8 +18610,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18852
18610
|
create: (context) => {
|
|
18853
18611
|
const options = resolveSettings$29(context.settings);
|
|
18854
18612
|
return { JSXOpeningElement(node) {
|
|
18855
|
-
|
|
18856
|
-
const elementName = resolveJsxElementType(node);
|
|
18613
|
+
const elementName = getElementName(node);
|
|
18857
18614
|
if (!elementName) return;
|
|
18858
18615
|
for (const attribute of node.attributes) {
|
|
18859
18616
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -19040,6 +18797,11 @@ const checkTarget = (attributeValue) => {
|
|
|
19040
18797
|
alternate: false
|
|
19041
18798
|
};
|
|
19042
18799
|
};
|
|
18800
|
+
const getOpeningElementName = (node) => {
|
|
18801
|
+
const name = node.name;
|
|
18802
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18803
|
+
return null;
|
|
18804
|
+
};
|
|
19043
18805
|
const jsxNoTargetBlank = defineRule({
|
|
19044
18806
|
id: "jsx-no-target-blank",
|
|
19045
18807
|
title: "Unsafe target=_blank link",
|
|
@@ -19059,8 +18821,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
19059
18821
|
return settings.formComponents.has(tagName);
|
|
19060
18822
|
};
|
|
19061
18823
|
return { JSXOpeningElement(node) {
|
|
19062
|
-
|
|
19063
|
-
const tagName = resolveJsxElementType(node);
|
|
18824
|
+
const tagName = getOpeningElementName(node);
|
|
19064
18825
|
if (!tagName) return;
|
|
19065
18826
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
19066
18827
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19293,7 +19054,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19293
19054
|
return {
|
|
19294
19055
|
JSXElement(node) {
|
|
19295
19056
|
const openingElement = node.openingElement;
|
|
19296
|
-
if (!isJsxFragmentElement(openingElement
|
|
19057
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
19297
19058
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19298
19059
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19299
19060
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20535,24 +20296,11 @@ const hasDirective = (programNode, directive) => {
|
|
|
20535
20296
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20536
20297
|
};
|
|
20537
20298
|
//#endregion
|
|
20538
|
-
//#region src/plugin/utils/
|
|
20539
|
-
const
|
|
20540
|
-
|
|
20541
|
-
|
|
20542
|
-
|
|
20543
|
-
]);
|
|
20544
|
-
const OBJECT_FREEZE_OR_SEAL_METHOD_NAMES = new Set(["freeze", "seal"]);
|
|
20545
|
-
const unwrapObjectIntegrityExpression = (node, scopes, methodNames = OBJECT_INTEGRITY_METHOD_NAMES) => {
|
|
20546
|
-
let expression = stripParenExpression(node);
|
|
20547
|
-
while (isNodeOfType(expression, "CallExpression")) {
|
|
20548
|
-
const callee = stripParenExpression(expression.callee);
|
|
20549
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Object" || !scopes.isGlobalReference(callee.object) || !isNodeOfType(callee.property, "Identifier") || !methodNames.has(callee.property.name)) break;
|
|
20550
|
-
const wrappedExpression = expression.arguments[0];
|
|
20551
|
-
if (!wrappedExpression || isNodeOfType(wrappedExpression, "SpreadElement")) break;
|
|
20552
|
-
expression = stripParenExpression(wrappedExpression);
|
|
20553
|
-
}
|
|
20554
|
-
return expression;
|
|
20555
|
-
};
|
|
20299
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20300
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20301
|
+
//#endregion
|
|
20302
|
+
//#region src/plugin/utils/is-component-assignment.ts
|
|
20303
|
+
const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
20556
20304
|
//#endregion
|
|
20557
20305
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20558
20306
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20578,10 +20326,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20578
20326
|
},
|
|
20579
20327
|
VariableDeclarator(node) {
|
|
20580
20328
|
if (!fileHasUseClient) return;
|
|
20329
|
+
if (!isComponentAssignment(node)) return;
|
|
20330
|
+
if (!isInlineFunctionExpression(node.init)) return;
|
|
20331
|
+
if (!node.init.async) return;
|
|
20581
20332
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
20582
|
-
if (!isUppercaseName(node.id.name) || !node.init) return;
|
|
20583
|
-
const componentFunction = unwrapObjectIntegrityExpression(node.init, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES);
|
|
20584
|
-
if (!isInlineFunctionExpression(componentFunction) || !componentFunction.async) return;
|
|
20585
20333
|
context.report({
|
|
20586
20334
|
node,
|
|
20587
20335
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -20883,7 +20631,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20883
20631
|
severity: "warn",
|
|
20884
20632
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20885
20633
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20886
|
-
if (
|
|
20634
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
20887
20635
|
const attributes = node.attributes ?? [];
|
|
20888
20636
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20889
20637
|
if (downloadAttribute) {
|
|
@@ -21079,7 +20827,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
21079
20827
|
severity: "warn",
|
|
21080
20828
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
21081
20829
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21082
|
-
if (
|
|
20830
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21083
20831
|
const attributes = node.attributes ?? [];
|
|
21084
20832
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
21085
20833
|
if (!relAttribute?.value) return;
|
|
@@ -21187,7 +20935,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21187
20935
|
severity: "warn",
|
|
21188
20936
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21189
20937
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21190
|
-
if (
|
|
20938
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21191
20939
|
const attributes = node.attributes ?? [];
|
|
21192
20940
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21193
20941
|
if (!hrefAttribute?.value) return;
|
|
@@ -21362,7 +21110,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21362
21110
|
create: (context) => {
|
|
21363
21111
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21364
21112
|
return { JSXOpeningElement(node) {
|
|
21365
|
-
if (
|
|
21113
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
21366
21114
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21367
21115
|
const programRoot = findProgramRoot(node);
|
|
21368
21116
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21409,8 +21157,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21409
21157
|
severity: "warn",
|
|
21410
21158
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21411
21159
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21412
|
-
|
|
21413
|
-
if (
|
|
21160
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
21161
|
+
if (node.name.name !== "script" && node.name.name !== "Script") return;
|
|
21414
21162
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21415
21163
|
if (!srcAttribute?.value) return;
|
|
21416
21164
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23218,15 +22966,10 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23218
22966
|
if (!declaringNode) return false;
|
|
23219
22967
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23220
22968
|
}));
|
|
23221
|
-
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23222
|
-
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23223
|
-
let declaringFunction = bindingNode.parent;
|
|
23224
|
-
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23225
|
-
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23226
|
-
};
|
|
23227
22969
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23228
22970
|
if (def.type !== "Parameter") return false;
|
|
23229
|
-
|
|
22971
|
+
const bindingParent = def.name.parent;
|
|
22972
|
+
return isFunctionLike$1(bindingParent);
|
|
23230
22973
|
}));
|
|
23231
22974
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23232
22975
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26223,56 +25966,6 @@ const noBarrelImport = defineRule({
|
|
|
26223
25966
|
}
|
|
26224
25967
|
});
|
|
26225
25968
|
//#endregion
|
|
26226
|
-
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26227
|
-
const collectReturnedExpressions = (functionNode) => {
|
|
26228
|
-
if (!isFunctionLike$1(functionNode)) return [];
|
|
26229
|
-
const body = functionNode.body;
|
|
26230
|
-
if (!body) return [];
|
|
26231
|
-
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26232
|
-
const returnedExpressions = [];
|
|
26233
|
-
walkAst(body, (node) => {
|
|
26234
|
-
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26235
|
-
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26236
|
-
});
|
|
26237
|
-
return returnedExpressions;
|
|
26238
|
-
};
|
|
26239
|
-
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26240
|
-
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26241
|
-
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26242
|
-
const functionMatches = (candidateFunction) => {
|
|
26243
|
-
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26244
|
-
visitedFunctions.add(candidateFunction);
|
|
26245
|
-
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26246
|
-
};
|
|
26247
|
-
const expressionMatches = (expression) => {
|
|
26248
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
26249
|
-
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26250
|
-
visitedExpressions.add(unwrappedExpression);
|
|
26251
|
-
if (matchesExpression(unwrappedExpression)) return true;
|
|
26252
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26253
|
-
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26254
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26255
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
26256
|
-
if (isFunctionLike$1(initializer)) return false;
|
|
26257
|
-
return expressionMatches(initializer);
|
|
26258
|
-
}
|
|
26259
|
-
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26260
|
-
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26261
|
-
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26262
|
-
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26263
|
-
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26264
|
-
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26265
|
-
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26266
|
-
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26267
|
-
return functionMatches(candidateFunction);
|
|
26268
|
-
}
|
|
26269
|
-
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26270
|
-
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26271
|
-
return false;
|
|
26272
|
-
};
|
|
26273
|
-
return functionMatches(functionNode);
|
|
26274
|
-
};
|
|
26275
|
-
//#endregion
|
|
26276
25969
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26277
25970
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26278
25971
|
"FunctionDeclaration",
|
|
@@ -26292,13 +25985,12 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26292
25985
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26293
25986
|
};
|
|
26294
25987
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26295
|
-
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
26296
25988
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26297
25989
|
let hasRenderOutput = false;
|
|
26298
25990
|
walkAst(rootNode, (node) => {
|
|
26299
25991
|
if (hasRenderOutput) return false;
|
|
26300
25992
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26301
|
-
if (
|
|
25993
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
|
|
26302
25994
|
hasRenderOutput = true;
|
|
26303
25995
|
return false;
|
|
26304
25996
|
}
|
|
@@ -26309,7 +26001,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26309
26001
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26310
26002
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26311
26003
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26312
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes)
|
|
26004
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26313
26005
|
renderOutputCache.set(functionNode, {
|
|
26314
26006
|
scopes,
|
|
26315
26007
|
hasRenderOutput
|
|
@@ -26317,9 +26009,6 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
26317
26009
|
return hasRenderOutput;
|
|
26318
26010
|
};
|
|
26319
26011
|
//#endregion
|
|
26320
|
-
//#region src/plugin/utils/is-component-assignment.ts
|
|
26321
|
-
const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
26322
|
-
//#endregion
|
|
26323
26012
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
26324
26013
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
26325
26014
|
//#endregion
|
|
@@ -28777,7 +28466,7 @@ const noDisabledZoom = defineRule({
|
|
|
28777
28466
|
category: "Accessibility",
|
|
28778
28467
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28779
28468
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28780
|
-
if (
|
|
28469
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
|
|
28781
28470
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28782
28471
|
if (!nameAttr?.value) return;
|
|
28783
28472
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -29167,7 +28856,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
29167
28856
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
29168
28857
|
for (const statement of componentBody.body ?? []) {
|
|
29169
28858
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
29170
|
-
const expression =
|
|
28859
|
+
const expression = statement.expression;
|
|
29171
28860
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
29172
28861
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
29173
28862
|
effectCalls.push(expression);
|
|
@@ -31949,7 +31638,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31949
31638
|
severity: "warn",
|
|
31950
31639
|
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
31951
31640
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31952
|
-
if (
|
|
31641
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
31953
31642
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31954
31643
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31955
31644
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31961,26 +31650,6 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31961
31650
|
} })
|
|
31962
31651
|
});
|
|
31963
31652
|
//#endregion
|
|
31964
|
-
//#region src/plugin/utils/react-ref-origin.ts
|
|
31965
|
-
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
31966
|
-
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
31967
|
-
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
31968
|
-
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
31969
|
-
if (!symbol?.initializer) return null;
|
|
31970
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
31971
|
-
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
31972
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
31973
|
-
};
|
|
31974
|
-
const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
31975
|
-
const expression = stripParenExpression(node);
|
|
31976
|
-
if (resolveReactRefSymbol(expression, scopes)) return true;
|
|
31977
|
-
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
31978
|
-
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
31979
|
-
if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
31980
|
-
visitedSymbolIds.add(symbol.id);
|
|
31981
|
-
return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
|
|
31982
|
-
};
|
|
31983
|
-
//#endregion
|
|
31984
31653
|
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
31985
31654
|
const TIMER_FUNCTION_NAMES = new Set([
|
|
31986
31655
|
"cancelAnimationFrame",
|
|
@@ -32012,33 +31681,6 @@ const NOTIFICATION_METHOD_NAMES = new Set([
|
|
|
32012
31681
|
"success",
|
|
32013
31682
|
"warning"
|
|
32014
31683
|
]);
|
|
32015
|
-
const NOTIFICATION_MODULE_SOURCES = new Set([
|
|
32016
|
-
"@chakra-ui/react",
|
|
32017
|
-
"@heroui/react",
|
|
32018
|
-
"@mantine/notifications",
|
|
32019
|
-
"antd",
|
|
32020
|
-
"react-hot-toast",
|
|
32021
|
-
"react-toastify",
|
|
32022
|
-
"sonner"
|
|
32023
|
-
]);
|
|
32024
|
-
const SYNCHRONOUS_ARRAY_METHOD_NAMES = new Set([
|
|
32025
|
-
"every",
|
|
32026
|
-
"filter",
|
|
32027
|
-
"find",
|
|
32028
|
-
"findIndex",
|
|
32029
|
-
"flatMap",
|
|
32030
|
-
"forEach",
|
|
32031
|
-
"map",
|
|
32032
|
-
"reduce",
|
|
32033
|
-
"reduceRight",
|
|
32034
|
-
"some",
|
|
32035
|
-
"sort"
|
|
32036
|
-
]);
|
|
32037
|
-
const isNotificationModuleSource = (source) => {
|
|
32038
|
-
if (!source) return false;
|
|
32039
|
-
for (const moduleSource of NOTIFICATION_MODULE_SOURCES) if (source === moduleSource || source.startsWith(`${moduleSource}/`)) return true;
|
|
32040
|
-
return /(?:^|[/_.-])(?:notification|toast)s?(?:$|[/_.-])/i.test(source);
|
|
32041
|
-
};
|
|
32042
31684
|
const getMemberCall = (node) => {
|
|
32043
31685
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
32044
31686
|
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
@@ -32050,37 +31692,27 @@ const getMemberCall = (node) => {
|
|
|
32050
31692
|
};
|
|
32051
31693
|
const isNotificationReceiver = (receiver, scopes) => {
|
|
32052
31694
|
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
32053
|
-
|
|
32054
|
-
if (importBinding) return isNotificationModuleSource(importBinding.source) && (importBinding.exportedName === "default" || importBinding.exportedName !== null && NOTIFICATION_RECEIVER_NAMES.has(importBinding.exportedName));
|
|
31695
|
+
if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
|
|
32055
31696
|
const symbol = scopes.symbolFor(receiver);
|
|
31697
|
+
if (symbol?.kind === "import") return true;
|
|
32056
31698
|
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
32057
31699
|
const callee = symbol.initializer.callee;
|
|
32058
|
-
|
|
32059
|
-
const hookImport = getImportBindingForName(callee, callee.name);
|
|
32060
|
-
return Boolean(hookImport && isNotificationModuleSource(hookImport.source) && hookImport.exportedName !== null && /^use(?:Message|Notification|Toast)$/.test(hookImport.exportedName));
|
|
32061
|
-
};
|
|
32062
|
-
const isKnownGlobalObject = (node, expectedName, scopes) => isNodeOfType(node, "Identifier") && node.name === expectedName && scopes.isGlobalReference(node);
|
|
32063
|
-
const isGlobalMember = (node, expectedPropertyName, scopes) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.property, "Identifier") && node.property.name === expectedPropertyName && (isKnownGlobalObject(node.object, "window", scopes) || isKnownGlobalObject(node.object, "globalThis", scopes));
|
|
32064
|
-
const isStorageReceiver = (node, scopes) => {
|
|
32065
|
-
if (isNodeOfType(node, "Identifier") && STORAGE_RECEIVER_NAMES.has(node.name) && scopes.isGlobalReference(node)) return true;
|
|
32066
|
-
for (const receiverName of STORAGE_RECEIVER_NAMES) if (isGlobalMember(node, receiverName, scopes)) return true;
|
|
32067
|
-
return false;
|
|
31700
|
+
return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
|
|
32068
31701
|
};
|
|
32069
31702
|
const getKnownImpureCall = (callExpression, scopes) => {
|
|
32070
31703
|
if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
|
|
32071
|
-
if (isNodeOfType(callExpression.callee, "MemberExpression") && !callExpression.callee.computed && isNodeOfType(callExpression.callee.property, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.property.name) && (isKnownGlobalObject(callExpression.callee.object, "window", scopes) || isKnownGlobalObject(callExpression.callee.object, "globalThis", scopes))) return `${callExpression.callee.property.name}()`;
|
|
32072
31704
|
const memberCall = getMemberCall(callExpression);
|
|
32073
31705
|
if (!memberCall) return null;
|
|
32074
31706
|
const { methodName, receiver } = memberCall;
|
|
32075
|
-
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) &&
|
|
32076
|
-
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)
|
|
32077
|
-
|
|
32078
|
-
if (EXTERNAL_READ_METHOD_NAMES.has(methodName) && receiverRoot !== null && isKnownGlobalObject(receiverRoot, "document", scopes)) return `.${methodName}()`;
|
|
32079
|
-
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNotificationReceiver(receiver, scopes)) return `${methodName}()`;
|
|
31707
|
+
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
|
|
31708
|
+
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
31709
|
+
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
|
|
32080
31710
|
return null;
|
|
32081
31711
|
};
|
|
32082
31712
|
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
32083
|
-
|
|
31713
|
+
let rootIdentifier = null;
|
|
31714
|
+
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
31715
|
+
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
32084
31716
|
if (!rootIdentifier) return null;
|
|
32085
31717
|
const updaterScope = scopes.ownScopeFor(updater);
|
|
32086
31718
|
if (!updaterScope) return null;
|
|
@@ -32089,26 +31721,12 @@ const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) =>
|
|
|
32089
31721
|
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
32090
31722
|
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
32091
31723
|
};
|
|
32092
|
-
const isArrayValue = (node, scopes) => {
|
|
32093
|
-
const expression = stripParenExpression(node);
|
|
32094
|
-
if (isNodeOfType(expression, "ArrayExpression")) return true;
|
|
32095
|
-
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
32096
|
-
return isNodeOfType(resolveConstIdentifierAlias(expression, scopes)?.initializer, "ArrayExpression");
|
|
32097
|
-
};
|
|
32098
|
-
const isDefinitelySynchronousCallback = (callback, scopes) => {
|
|
32099
|
-
const parent = callback.parent;
|
|
32100
|
-
if (!parent) return false;
|
|
32101
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee === callback) return true;
|
|
32102
|
-
if (!isNodeOfType(parent, "CallExpression") || !parent.arguments?.some((argument) => argument === callback)) return false;
|
|
32103
|
-
if (isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ARRAY_METHOD_NAMES.has(parent.callee.property.name) && isArrayValue(parent.callee.object, scopes)) return true;
|
|
32104
|
-
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.object, "Identifier") && parent.callee.object.name === "Array" && scopes.isGlobalReference(parent.callee.object) && isNodeOfType(parent.callee.property, "Identifier") && parent.callee.property.name === "from" && parent.arguments[1] === callback;
|
|
32105
|
-
};
|
|
32106
31724
|
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
32107
31725
|
const analysis = getProgramAnalysis(updater);
|
|
32108
31726
|
let operation = null;
|
|
32109
31727
|
walkAst(updater, (child) => {
|
|
32110
31728
|
if (operation) return false;
|
|
32111
|
-
if (child !== updater && isFunctionLike$1(child) && !
|
|
31729
|
+
if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
|
|
32112
31730
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32113
31731
|
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
32114
31732
|
const calleeReference = getRef(analysis, child.callee);
|
|
@@ -32140,25 +31758,18 @@ const noImpureStateUpdater = defineRule({
|
|
|
32140
31758
|
severity: "error",
|
|
32141
31759
|
recommendation: "Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.",
|
|
32142
31760
|
create: (context) => ({ CallExpression(node) {
|
|
32143
|
-
const
|
|
32144
|
-
if (!
|
|
31761
|
+
const updater = node.arguments?.[0];
|
|
31762
|
+
if (!updater || !isFunctionLike$1(updater)) return;
|
|
32145
31763
|
const analysis = getProgramAnalysis(node);
|
|
32146
31764
|
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
32147
31765
|
const calleeReference = getRef(analysis, node.callee);
|
|
32148
31766
|
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
32149
31767
|
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
32150
31768
|
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
32151
|
-
let updater = null;
|
|
32152
|
-
if (isFunctionLike$1(updaterArgument)) updater = updaterArgument;
|
|
32153
|
-
else if (isNodeOfType(updaterArgument, "Identifier")) {
|
|
32154
|
-
const updaterReference = getRef(analysis, updaterArgument);
|
|
32155
|
-
if (updaterReference) updater = resolveToFunction(updaterReference);
|
|
32156
|
-
}
|
|
32157
|
-
if (!updater) return;
|
|
32158
31769
|
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
32159
31770
|
if (!operation) return;
|
|
32160
31771
|
context.report({
|
|
32161
|
-
node:
|
|
31772
|
+
node: updater,
|
|
32162
31773
|
message: `This state updater performs ${operation}. React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.`
|
|
32163
31774
|
});
|
|
32164
31775
|
} })
|
|
@@ -32268,7 +31879,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
32268
31879
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
32269
31880
|
return {
|
|
32270
31881
|
JSXOpeningElement(node) {
|
|
32271
|
-
if (
|
|
31882
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
32272
31883
|
let typeAttribute = null;
|
|
32273
31884
|
let typeAttributeIndex = null;
|
|
32274
31885
|
let indeterminateAttribute = null;
|
|
@@ -32447,12 +32058,11 @@ const isMemoCall = (node) => {
|
|
|
32447
32058
|
};
|
|
32448
32059
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
32449
32060
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
32450
|
-
const isInlineReference = (node
|
|
32451
|
-
|
|
32452
|
-
if (isNodeOfType(
|
|
32453
|
-
if (isNodeOfType(
|
|
32454
|
-
if (isNodeOfType(
|
|
32455
|
-
if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
|
|
32061
|
+
const isInlineReference = (node) => {
|
|
32062
|
+
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "bind") return "functions";
|
|
32063
|
+
if (isNodeOfType(node, "ObjectExpression")) return "objects";
|
|
32064
|
+
if (isNodeOfType(node, "ArrayExpression")) return "Arrays";
|
|
32065
|
+
if (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")) return "JSX";
|
|
32456
32066
|
return null;
|
|
32457
32067
|
};
|
|
32458
32068
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -32482,7 +32092,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
32482
32092
|
let elementName = null;
|
|
32483
32093
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
32484
32094
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
32485
|
-
const propType = isInlineReference(node.value.expression
|
|
32095
|
+
const propType = isInlineReference(node.value.expression);
|
|
32486
32096
|
if (propType) context.report({
|
|
32487
32097
|
node: node.value.expression,
|
|
32488
32098
|
message: `This redraws ${elementName} on every render because the prop is ${propType} built right here, so memo() can't skip it. Move it to a stable value with useMemo, useCallback, or module scope`
|
|
@@ -33003,18 +32613,6 @@ const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
|
33003
32613
|
"formatToParts",
|
|
33004
32614
|
"formatRange"
|
|
33005
32615
|
]);
|
|
33006
|
-
const ABSENT_PROPERTY_PROOF = { status: "absent" };
|
|
33007
|
-
const PRESENT_PROPERTY_PROOF = { status: "present" };
|
|
33008
|
-
const UNDEFINED_PROPERTY_PROOF = { status: "undefined" };
|
|
33009
|
-
const UNKNOWN_PROPERTY_PROOF = { status: "unknown" };
|
|
33010
|
-
const READ_ONLY_OBJECT_METHOD_NAMES = new Set([
|
|
33011
|
-
"hasOwnProperty",
|
|
33012
|
-
"isPrototypeOf",
|
|
33013
|
-
"propertyIsEnumerable",
|
|
33014
|
-
"toLocaleString",
|
|
33015
|
-
"toString",
|
|
33016
|
-
"valueOf"
|
|
33017
|
-
]);
|
|
33018
32616
|
const isProvableDateExpression = (expression) => {
|
|
33019
32617
|
if (!expression) return false;
|
|
33020
32618
|
const unwrapped = stripParenExpression(expression);
|
|
@@ -33029,290 +32627,32 @@ const receiverNameLooksDateFlavored = (expression) => {
|
|
|
33029
32627
|
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
33030
32628
|
return false;
|
|
33031
32629
|
};
|
|
33032
|
-
const
|
|
33033
|
-
|
|
33034
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return true;
|
|
33035
|
-
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
33036
|
-
if (expression.name === "undefined" && scopes.isGlobalReference(expression)) return true;
|
|
33037
|
-
const symbol = scopes.symbolFor(expression);
|
|
33038
|
-
if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || visitedSymbolIds.has(symbol.id)) return false;
|
|
33039
|
-
visitedSymbolIds.add(symbol.id);
|
|
33040
|
-
return isStaticUndefined(symbol.initializer, scopes, visitedSymbolIds);
|
|
33041
|
-
};
|
|
33042
|
-
const isNestedAssignmentTarget = (expression) => {
|
|
33043
|
-
let target = expression;
|
|
33044
|
-
let parent = target.parent;
|
|
33045
|
-
while (parent) {
|
|
33046
|
-
if (isNodeOfType(parent, "AssignmentExpression")) return parent.left === target;
|
|
33047
|
-
if (isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement")) return parent.left === target;
|
|
33048
|
-
if (isNodeOfType(parent, "AssignmentPattern")) {
|
|
33049
|
-
if (parent.left !== target) return false;
|
|
33050
|
-
} else if (isNodeOfType(parent, "RestElement")) {
|
|
33051
|
-
if (parent.argument !== target) return false;
|
|
33052
|
-
} else if (isNodeOfType(parent, "ArrayPattern")) {
|
|
33053
|
-
if (!parent.elements?.some((element) => element === target)) return false;
|
|
33054
|
-
} else if (isNodeOfType(parent, "Property")) {
|
|
33055
|
-
if (parent.value !== target || !isNodeOfType(parent.parent, "ObjectPattern")) return false;
|
|
33056
|
-
} else if (isNodeOfType(parent, "ObjectPattern")) {
|
|
33057
|
-
if (!parent.properties?.some((property) => property === target)) return false;
|
|
33058
|
-
} else return false;
|
|
33059
|
-
target = parent;
|
|
33060
|
-
parent = target.parent;
|
|
33061
|
-
}
|
|
33062
|
-
return false;
|
|
33063
|
-
};
|
|
33064
|
-
const isPotentialMutationReference = (identifier, readNode) => {
|
|
33065
|
-
let expression = identifier;
|
|
33066
|
-
let parent = expression.parent;
|
|
33067
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
|
|
33068
|
-
expression = parent;
|
|
33069
|
-
parent = expression.parent;
|
|
33070
|
-
}
|
|
33071
|
-
const rootExpression = expression;
|
|
33072
|
-
let memberDepth = 0;
|
|
33073
|
-
while (parent && isNodeOfType(parent, "MemberExpression") && parent.object === expression) {
|
|
33074
|
-
memberDepth += 1;
|
|
33075
|
-
expression = parent;
|
|
33076
|
-
parent = expression.parent;
|
|
33077
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
|
|
33078
|
-
expression = parent;
|
|
33079
|
-
parent = expression.parent;
|
|
33080
|
-
}
|
|
33081
|
-
}
|
|
33082
|
-
if (!parent || isAstDescendant(identifier, readNode)) return false;
|
|
33083
|
-
if (isNestedAssignmentTarget(expression)) return true;
|
|
33084
|
-
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === expression) return true;
|
|
33085
|
-
if (isNodeOfType(parent, "UpdateExpression") && parent.argument === expression) return true;
|
|
33086
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expression) return true;
|
|
33087
|
-
if (isNodeOfType(parent, "CallExpression")) {
|
|
33088
|
-
if (parent.callee === expression) {
|
|
33089
|
-
if (memberDepth === 0) return true;
|
|
33090
|
-
const memberExpression = stripParenExpression(expression);
|
|
33091
|
-
return memberDepth === 1 && isNodeOfType(memberExpression, "MemberExpression") && !READ_ONLY_OBJECT_METHOD_NAMES.has(getStaticPropertyName(memberExpression) ?? "");
|
|
33092
|
-
}
|
|
33093
|
-
return memberDepth === 0 && parent.arguments?.some((argument) => argument === rootExpression);
|
|
33094
|
-
}
|
|
33095
|
-
if (isNodeOfType(parent, "NewExpression")) return memberDepth === 0 && parent.arguments?.some((argument) => argument === rootExpression);
|
|
33096
|
-
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === rootExpression) return true;
|
|
33097
|
-
return false;
|
|
33098
|
-
};
|
|
33099
|
-
const getSimpleAlias = (identifier, scopes) => {
|
|
33100
|
-
let expression = identifier;
|
|
33101
|
-
let parent = expression.parent;
|
|
33102
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
|
|
33103
|
-
expression = parent;
|
|
33104
|
-
parent = expression.parent;
|
|
33105
|
-
}
|
|
33106
|
-
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== expression || !isNodeOfType(parent.id, "Identifier")) return null;
|
|
33107
|
-
const symbol = scopes.symbolFor(parent.id);
|
|
33108
|
-
return symbol ? {
|
|
33109
|
-
symbol,
|
|
33110
|
-
readNode: parent.id
|
|
33111
|
-
} : null;
|
|
33112
|
-
};
|
|
33113
|
-
const getDirectCallForExpression = (expression) => {
|
|
33114
|
-
let callee = expression;
|
|
33115
|
-
let parent = callee.parent;
|
|
33116
|
-
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === callee) {
|
|
33117
|
-
callee = parent;
|
|
33118
|
-
parent = callee.parent;
|
|
33119
|
-
}
|
|
33120
|
-
return isNodeOfType(parent, "CallExpression") && parent.callee === callee ? parent : null;
|
|
33121
|
-
};
|
|
33122
|
-
const getMethodOwner = (functionNode, scopes) => {
|
|
33123
|
-
const methodNode = functionNode.parent;
|
|
33124
|
-
if (isNodeOfType(methodNode, "Property") && methodNode.value === functionNode && isNodeOfType(methodNode.parent, "ObjectExpression")) {
|
|
33125
|
-
const objectParent = methodNode.parent.parent;
|
|
33126
|
-
if (!isNodeOfType(objectParent, "VariableDeclarator") || objectParent.init !== methodNode.parent || !isNodeOfType(objectParent.id, "Identifier")) return null;
|
|
33127
|
-
const ownerSymbol = scopes.symbolFor(objectParent.id);
|
|
33128
|
-
const methodName = getStaticPropertyKeyName(methodNode, { allowComputedString: true });
|
|
33129
|
-
return ownerSymbol && methodName ? {
|
|
33130
|
-
methodName,
|
|
33131
|
-
ownerKind: "object",
|
|
33132
|
-
ownerSymbol,
|
|
33133
|
-
isStatic: false
|
|
33134
|
-
} : null;
|
|
33135
|
-
}
|
|
33136
|
-
if (!isNodeOfType(methodNode, "MethodDefinition") || methodNode.value !== functionNode || !isNodeOfType(methodNode.parent, "ClassBody")) return null;
|
|
33137
|
-
const classNode = methodNode.parent.parent;
|
|
33138
|
-
if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression")) return null;
|
|
33139
|
-
let bindingIdentifier = isNodeOfType(classNode.id, "Identifier") ? classNode.id : null;
|
|
33140
|
-
if (!bindingIdentifier && isNodeOfType(classNode.parent, "VariableDeclarator")) bindingIdentifier = isNodeOfType(classNode.parent.id, "Identifier") ? classNode.parent.id : null;
|
|
33141
|
-
if (!bindingIdentifier) return null;
|
|
33142
|
-
const ownerSymbol = scopes.symbolFor(bindingIdentifier);
|
|
33143
|
-
const methodName = getStaticPropertyKeyName(methodNode, { allowComputedString: true });
|
|
33144
|
-
return ownerSymbol && methodName ? {
|
|
33145
|
-
methodName,
|
|
33146
|
-
ownerKind: "class",
|
|
33147
|
-
ownerSymbol,
|
|
33148
|
-
isStatic: Boolean(methodNode.static)
|
|
33149
|
-
} : null;
|
|
33150
|
-
};
|
|
33151
|
-
const doesSymbolResolveToOwner = (symbol, ownerSymbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
33152
|
-
if (!symbol) return false;
|
|
33153
|
-
if (symbol.declarationNode === ownerSymbol.declarationNode) return true;
|
|
33154
|
-
if (symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
33155
|
-
visitedSymbolIds.add(symbol.id);
|
|
33156
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
33157
|
-
return isNodeOfType(initializer, "Identifier") && doesSymbolResolveToOwner(scopes.symbolFor(initializer), ownerSymbol, scopes, visitedSymbolIds);
|
|
33158
|
-
};
|
|
33159
|
-
const isClassInstanceExpression = (expression, ownerSymbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
33160
|
-
const candidate = stripParenExpression(expression);
|
|
33161
|
-
if (isNodeOfType(candidate, "NewExpression") && isNodeOfType(candidate.callee, "Identifier")) return doesSymbolResolveToOwner(scopes.symbolFor(candidate.callee), ownerSymbol, scopes);
|
|
33162
|
-
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
33163
|
-
const symbol = scopes.symbolFor(candidate);
|
|
33164
|
-
if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
33165
|
-
visitedSymbolIds.add(symbol.id);
|
|
33166
|
-
return isClassInstanceExpression(symbol.initializer, ownerSymbol, scopes, visitedSymbolIds);
|
|
33167
|
-
};
|
|
33168
|
-
const getMethodCalls = (functionNode, scopes) => {
|
|
33169
|
-
const owner = getMethodOwner(functionNode, scopes);
|
|
33170
|
-
if (!owner) return [];
|
|
33171
|
-
const calls = [];
|
|
33172
|
-
walkAst(scopes.rootScope.node, (child) => {
|
|
33173
|
-
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
33174
|
-
if (getStaticPropertyName(child) !== owner.methodName) return;
|
|
33175
|
-
const receiver = stripParenExpression(child.object);
|
|
33176
|
-
let doesReceiverMatch = isNodeOfType(receiver, "Identifier") && doesSymbolResolveToOwner(scopes.symbolFor(receiver), owner.ownerSymbol, scopes);
|
|
33177
|
-
if (owner.ownerKind === "class" && !owner.isStatic) doesReceiverMatch = isClassInstanceExpression(receiver, owner.ownerSymbol, scopes);
|
|
33178
|
-
if (!doesReceiverMatch) return;
|
|
33179
|
-
const call = getDirectCallForExpression(child);
|
|
33180
|
-
if (call) calls.push(call);
|
|
33181
|
-
});
|
|
33182
|
-
return calls;
|
|
33183
|
-
};
|
|
33184
|
-
const isFunctionInvokedBeforeUsage = (functionNode, usageNode, usageBoundary, scopes, visitedSymbolIds, visitedFunctionNodes = /* @__PURE__ */ new Set()) => {
|
|
33185
|
-
if (visitedFunctionNodes.has(functionNode)) return false;
|
|
33186
|
-
visitedFunctionNodes.add(functionNode);
|
|
33187
|
-
const usageFunction = findEnclosingFunction(usageNode);
|
|
33188
|
-
const immediateCall = getDirectCallForExpression(functionNode);
|
|
33189
|
-
if (immediateCall) {
|
|
33190
|
-
const immediateCallFunction = findEnclosingFunction(immediateCall);
|
|
33191
|
-
if (immediateCallFunction === usageFunction) {
|
|
33192
|
-
const immediateCallStart = getRangeStart(immediateCall);
|
|
33193
|
-
return immediateCallStart === null || immediateCallStart < usageBoundary;
|
|
33194
|
-
}
|
|
33195
|
-
if (!immediateCallFunction) return usageFunction !== null;
|
|
33196
|
-
return isFunctionInvokedBeforeUsage(immediateCallFunction, usageNode, usageBoundary, scopes, visitedSymbolIds, new Set(visitedFunctionNodes));
|
|
33197
|
-
}
|
|
33198
|
-
for (const methodCall of getMethodCalls(functionNode, scopes)) {
|
|
33199
|
-
const methodCallFunction = findEnclosingFunction(methodCall);
|
|
33200
|
-
if (methodCallFunction === usageFunction) {
|
|
33201
|
-
const methodCallStart = getRangeStart(methodCall);
|
|
33202
|
-
if (methodCallStart === null || methodCallStart < usageBoundary) return true;
|
|
33203
|
-
continue;
|
|
33204
|
-
}
|
|
33205
|
-
if (!methodCallFunction) {
|
|
33206
|
-
if (usageFunction) return true;
|
|
33207
|
-
continue;
|
|
33208
|
-
}
|
|
33209
|
-
if (isFunctionInvokedBeforeUsage(methodCallFunction, usageNode, usageBoundary, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes))) return true;
|
|
33210
|
-
}
|
|
33211
|
-
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
33212
|
-
if (!bindingIdentifier) return false;
|
|
33213
|
-
const symbol = scopes.symbolFor(bindingIdentifier);
|
|
33214
|
-
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
33215
|
-
visitedSymbolIds.add(symbol.id);
|
|
33216
|
-
let wasInvokedBeforeUsage = false;
|
|
33217
|
-
walkAst(scopes.rootScope.node, (child) => {
|
|
33218
|
-
if (wasInvokedBeforeUsage || !isNodeOfType(child, "Identifier")) return;
|
|
33219
|
-
if (scopes.symbolFor(child)?.declarationNode !== symbol.declarationNode) return;
|
|
33220
|
-
const call = getDirectCallForExpression(child);
|
|
33221
|
-
if (!call) return false;
|
|
33222
|
-
const callFunction = findEnclosingFunction(call);
|
|
33223
|
-
if (callFunction === usageFunction) {
|
|
33224
|
-
const callStart = getRangeStart(call);
|
|
33225
|
-
wasInvokedBeforeUsage = callStart === null || callStart < usageBoundary;
|
|
33226
|
-
return;
|
|
33227
|
-
}
|
|
33228
|
-
if (!callFunction) {
|
|
33229
|
-
wasInvokedBeforeUsage = usageFunction !== null;
|
|
33230
|
-
return;
|
|
33231
|
-
}
|
|
33232
|
-
wasInvokedBeforeUsage = isFunctionInvokedBeforeUsage(callFunction, usageNode, usageBoundary, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes));
|
|
33233
|
-
});
|
|
33234
|
-
return wasInvokedBeforeUsage;
|
|
33235
|
-
};
|
|
33236
|
-
const getMutationUsageBoundary = (usageNode, readNode) => {
|
|
33237
|
-
const readStart = getRangeStart(readNode);
|
|
33238
|
-
let readExpression = readNode;
|
|
33239
|
-
let readParent = readExpression.parent;
|
|
33240
|
-
while (readParent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(readParent.type) && "expression" in readParent && readParent.expression === readExpression) {
|
|
33241
|
-
readExpression = readParent;
|
|
33242
|
-
readParent = readExpression.parent;
|
|
33243
|
-
}
|
|
33244
|
-
if ((isNodeOfType(usageNode, "CallExpression") || isNodeOfType(usageNode, "NewExpression")) && usageNode.arguments?.some((argument) => argument === readExpression)) return usageNode.range?.[1] ?? null;
|
|
33245
|
-
if (isAstDescendant(readNode, usageNode)) return readStart;
|
|
33246
|
-
return getRangeStart(usageNode);
|
|
33247
|
-
};
|
|
33248
|
-
const wasMutatedBeforeUsage = (symbol, usageNode, readNode, scopes, visitedMutationSymbolIds = /* @__PURE__ */ new Set(), inheritedUsageBoundary, readNodesBySymbolId = /* @__PURE__ */ new Map()) => {
|
|
33249
|
-
if (visitedMutationSymbolIds.has(symbol.id)) return false;
|
|
33250
|
-
visitedMutationSymbolIds.add(symbol.id);
|
|
33251
|
-
const usageBoundary = inheritedUsageBoundary === void 0 ? getMutationUsageBoundary(usageNode, readNode) : inheritedUsageBoundary;
|
|
33252
|
-
if (typeof usageBoundary !== "number") return true;
|
|
33253
|
-
const usageFunction = findEnclosingFunction(usageNode);
|
|
33254
|
-
return symbol.references.some((reference) => {
|
|
33255
|
-
const referenceStart = getRangeStart(reference.identifier);
|
|
33256
|
-
const simpleAlias = getSimpleAlias(reference.identifier, scopes);
|
|
33257
|
-
if (simpleAlias) return wasMutatedBeforeUsage(simpleAlias.symbol, usageNode, readNodesBySymbolId.get(simpleAlias.symbol.id) ?? simpleAlias.readNode, scopes, new Set(visitedMutationSymbolIds), usageBoundary, readNodesBySymbolId);
|
|
33258
|
-
if (!isPotentialMutationReference(reference.identifier, readNode)) return false;
|
|
33259
|
-
if (referenceStart === null) return true;
|
|
33260
|
-
const mutationFunction = findEnclosingFunction(reference.identifier);
|
|
33261
|
-
if (mutationFunction === usageFunction) return referenceStart < usageBoundary;
|
|
33262
|
-
if (!mutationFunction) return usageFunction !== null;
|
|
33263
|
-
if (usageFunction && isAstDescendant(usageFunction, mutationFunction)) return true;
|
|
33264
|
-
return isFunctionInvokedBeforeUsage(mutationFunction, usageNode, usageBoundary, scopes, /* @__PURE__ */ new Set());
|
|
33265
|
-
});
|
|
33266
|
-
};
|
|
33267
|
-
const getObjectPropertyProof = (objectExpression, propertyName, scopes, usageNode, visitedSymbolIds = /* @__PURE__ */ new Set(), inheritedUsageBoundary, readNodesBySymbolId = /* @__PURE__ */ new Map()) => {
|
|
33268
|
-
if (!objectExpression) return ABSENT_PROPERTY_PROOF;
|
|
32630
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
32631
|
+
if (!objectExpression) return false;
|
|
33269
32632
|
const unwrapped = stripParenExpression(objectExpression);
|
|
33270
|
-
if (isNodeOfType(unwrapped, "
|
|
33271
|
-
|
|
33272
|
-
|
|
33273
|
-
|
|
33274
|
-
if (
|
|
33275
|
-
|
|
33276
|
-
if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || visitedSymbolIds.has(symbol.id) || wasMutatedBeforeUsage(symbol, usageNode, unwrapped, scopes, /* @__PURE__ */ new Set(), usageBoundary, nextReadNodesBySymbolId)) return UNKNOWN_PROPERTY_PROOF;
|
|
33277
|
-
visitedSymbolIds.add(symbol.id);
|
|
33278
|
-
return getObjectPropertyProof(symbol.initializer, propertyName, scopes, usageNode, visitedSymbolIds, usageBoundary, nextReadNodesBySymbolId);
|
|
33279
|
-
}
|
|
33280
|
-
if (isNodeOfType(unwrapped, "ConditionalExpression")) {
|
|
33281
|
-
const consequent = getObjectPropertyProof(unwrapped.consequent, propertyName, scopes, usageNode, new Set(visitedSymbolIds), inheritedUsageBoundary, new Map(readNodesBySymbolId));
|
|
33282
|
-
const alternate = getObjectPropertyProof(unwrapped.alternate, propertyName, scopes, usageNode, new Set(visitedSymbolIds), inheritedUsageBoundary, new Map(readNodesBySymbolId));
|
|
33283
|
-
return consequent.status === alternate.status ? consequent : UNKNOWN_PROPERTY_PROOF;
|
|
33284
|
-
}
|
|
33285
|
-
if (isNodeOfType(unwrapped, "CallExpression") && isNodeOfType(unwrapped.callee, "MemberExpression") && !unwrapped.callee.computed && isNodeOfType(unwrapped.callee.object, "Identifier") && unwrapped.callee.object.name === "Object" && scopes.isGlobalReference(unwrapped.callee.object) && isNodeOfType(unwrapped.callee.property, "Identifier") && unwrapped.callee.property.name === "freeze") return getObjectPropertyProof(unwrapped.arguments?.[0], propertyName, scopes, usageNode, visitedSymbolIds, inheritedUsageBoundary, readNodesBySymbolId);
|
|
33286
|
-
if (!isNodeOfType(unwrapped, "ObjectExpression")) return UNKNOWN_PROPERTY_PROOF;
|
|
33287
|
-
const properties = unwrapped.properties ?? [];
|
|
33288
|
-
for (let propertyIndex = properties.length - 1; propertyIndex >= 0; propertyIndex -= 1) {
|
|
33289
|
-
const property = properties[propertyIndex];
|
|
33290
|
-
if (!property) continue;
|
|
33291
|
-
if (isNodeOfType(property, "SpreadElement")) {
|
|
33292
|
-
const spreadProof = getObjectPropertyProof(property.argument, propertyName, scopes, unwrapped, new Set(visitedSymbolIds), void 0);
|
|
33293
|
-
if (spreadProof.status !== "absent") return spreadProof;
|
|
33294
|
-
continue;
|
|
33295
|
-
}
|
|
33296
|
-
if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== propertyName) continue;
|
|
33297
|
-
return isStaticUndefined(property.value, scopes) ? UNDEFINED_PROPERTY_PROOF : PRESENT_PROPERTY_PROOF;
|
|
32633
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
32634
|
+
for (const property of unwrapped.properties ?? []) {
|
|
32635
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
32636
|
+
if (property.computed) continue;
|
|
32637
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
32638
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
33298
32639
|
}
|
|
33299
|
-
return
|
|
32640
|
+
return false;
|
|
33300
32641
|
};
|
|
33301
|
-
const
|
|
33302
|
-
const hasExplicitLocaleArgument = (argument, scopes) => {
|
|
32642
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
33303
32643
|
if (!argument) return false;
|
|
33304
32644
|
const unwrapped = stripParenExpression(argument);
|
|
33305
|
-
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined"
|
|
32645
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
33306
32646
|
return true;
|
|
33307
32647
|
};
|
|
33308
|
-
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate
|
|
32648
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
33309
32649
|
const localeArgument = call.arguments?.[0];
|
|
33310
|
-
if (!hasExplicitLocaleArgument(localeArgument
|
|
32650
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
33311
32651
|
const optionsArgument = call.arguments?.[1];
|
|
33312
|
-
if (
|
|
32652
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
33313
32653
|
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
33314
32654
|
};
|
|
33315
|
-
const matchLocaleMethodCall = (call
|
|
32655
|
+
const matchLocaleMethodCall = (call) => {
|
|
33316
32656
|
const callee = call.callee;
|
|
33317
32657
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
33318
32658
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
@@ -33320,7 +32660,7 @@ const matchLocaleMethodCall = (call, scopes) => {
|
|
|
33320
32660
|
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
33321
32661
|
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
33322
32662
|
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
33323
|
-
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate
|
|
32663
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
33324
32664
|
return {
|
|
33325
32665
|
node: call,
|
|
33326
32666
|
display: `${methodName}()`
|
|
@@ -33336,13 +32676,13 @@ const getIntlFormatterName = (expression) => {
|
|
|
33336
32676
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
33337
32677
|
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
33338
32678
|
};
|
|
33339
|
-
const isDeterministicIntlConstruction = (construction, formatterName
|
|
32679
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
33340
32680
|
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
33341
|
-
if (!hasExplicitLocaleArgument(construction.arguments?.[0]
|
|
32681
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
33342
32682
|
if (formatterName !== "DateTimeFormat") return true;
|
|
33343
|
-
return
|
|
32683
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
33344
32684
|
};
|
|
33345
|
-
const matchIntlFormatCall = (call
|
|
32685
|
+
const matchIntlFormatCall = (call) => {
|
|
33346
32686
|
const callee = call.callee;
|
|
33347
32687
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
33348
32688
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
@@ -33355,7 +32695,7 @@ const matchIntlFormatCall = (call, scopes) => {
|
|
|
33355
32695
|
if (!construction) return null;
|
|
33356
32696
|
const formatterName = getIntlFormatterName(construction);
|
|
33357
32697
|
if (!formatterName) return null;
|
|
33358
|
-
if (isDeterministicIntlConstruction(construction, formatterName
|
|
32698
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
33359
32699
|
return {
|
|
33360
32700
|
node: call,
|
|
33361
32701
|
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
@@ -33424,7 +32764,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
33424
32764
|
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
33425
32765
|
},
|
|
33426
32766
|
CallExpression(node) {
|
|
33427
|
-
const match = matchLocaleMethodCall(node
|
|
32767
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
33428
32768
|
if (match) reportIfRenderPhase(match);
|
|
33429
32769
|
},
|
|
33430
32770
|
TemplateLiteral(node) {
|
|
@@ -33444,7 +32784,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
33444
32784
|
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
33445
32785
|
if (isFunctionLike$1(child)) return false;
|
|
33446
32786
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
33447
|
-
const match = matchLocaleMethodCall(child
|
|
32787
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
33448
32788
|
if (!match || reportedNodes.has(match.node)) return;
|
|
33449
32789
|
if (fileIsEmailTemplate) return;
|
|
33450
32790
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
@@ -33611,13 +32951,11 @@ const noManyBooleanProps = defineRule({
|
|
|
33611
32951
|
};
|
|
33612
32952
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33613
32953
|
if (!param) return;
|
|
33614
|
-
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33615
|
-
if (!propsBinding) return;
|
|
33616
32954
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33617
|
-
if (isNodeOfType(
|
|
32955
|
+
if (isNodeOfType(param, "ObjectPattern")) {
|
|
33618
32956
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33619
32957
|
const booleanLikePropNames = [];
|
|
33620
|
-
for (const property of
|
|
32958
|
+
for (const property of param.properties ?? []) {
|
|
33621
32959
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33622
32960
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33623
32961
|
if (!keyName) continue;
|
|
@@ -33628,7 +32966,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33628
32966
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33629
32967
|
return;
|
|
33630
32968
|
}
|
|
33631
|
-
if (isNodeOfType(
|
|
32969
|
+
if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
|
|
33632
32970
|
};
|
|
33633
32971
|
return {
|
|
33634
32972
|
FunctionDeclaration(node) {
|
|
@@ -33750,7 +33088,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33750
33088
|
if (mirrorBindings.length === 0) return;
|
|
33751
33089
|
for (const statement of componentBody.body ?? []) {
|
|
33752
33090
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
33753
|
-
const effectCall =
|
|
33091
|
+
const effectCall = statement.expression;
|
|
33754
33092
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
33755
33093
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
33756
33094
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -33764,7 +33102,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33764
33102
|
const bodyStatements = getCallbackStatements(callback);
|
|
33765
33103
|
if (bodyStatements.length !== 1) continue;
|
|
33766
33104
|
const onlyStatement = bodyStatements[0];
|
|
33767
|
-
const expression =
|
|
33105
|
+
const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
|
|
33768
33106
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
33769
33107
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
33770
33108
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -36380,7 +35718,7 @@ const noPreventDefault = defineRule({
|
|
|
36380
35718
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
36381
35719
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
36382
35720
|
return { JSXOpeningElement(node) {
|
|
36383
|
-
const elementName =
|
|
35721
|
+
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
36384
35722
|
if (!elementName) return;
|
|
36385
35723
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
36386
35724
|
if (!targetEventProps) return;
|
|
@@ -37270,85 +36608,28 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
37270
36608
|
});
|
|
37271
36609
|
//#endregion
|
|
37272
36610
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
37273
|
-
const
|
|
37274
|
-
"
|
|
37275
|
-
"
|
|
37276
|
-
|
|
37277
|
-
|
|
37278
|
-
|
|
37279
|
-
|
|
36611
|
+
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
36612
|
+
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
36613
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
36614
|
+
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
36615
|
+
if (!symbol?.initializer) return null;
|
|
36616
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
36617
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
36618
|
+
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
36619
|
+
};
|
|
37280
36620
|
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
37281
|
-
if (!isNodeOfType(node, "MemberExpression") ||
|
|
36621
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
37282
36622
|
const receiver = stripParenExpression(node.object);
|
|
37283
36623
|
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
37284
36624
|
};
|
|
37285
|
-
const isSameRefCurrentAlias = (node, refSymbol, scopes) => {
|
|
37286
|
-
if (isSameRefCurrentMember(node, refSymbol, scopes)) return true;
|
|
37287
|
-
if (!isNodeOfType(node, "Identifier")) return false;
|
|
37288
|
-
const aliasSymbol = scopes.symbolFor(node);
|
|
37289
|
-
return aliasSymbol?.kind === "const" && aliasSymbol.initializer !== null && isSameRefCurrentMember(stripParenExpression(aliasSymbol.initializer), refSymbol, scopes);
|
|
37290
|
-
};
|
|
37291
|
-
const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
|
|
37292
|
-
const hasRepeatedExecutionAncestor = (node, stop) => {
|
|
37293
|
-
let ancestor = node.parent;
|
|
37294
|
-
while (ancestor && ancestor !== stop) {
|
|
37295
|
-
if (isFunctionLike$1(ancestor) || REPEATED_ANCESTOR_TYPES.has(ancestor.type)) return true;
|
|
37296
|
-
ancestor = ancestor.parent;
|
|
37297
|
-
}
|
|
37298
|
-
return ancestor !== stop;
|
|
37299
|
-
};
|
|
37300
|
-
const getBranchConstraints = (node, branchRoot) => {
|
|
37301
|
-
const constraints = /* @__PURE__ */ new Map();
|
|
37302
|
-
let descendant = node;
|
|
37303
|
-
let ancestor = descendant.parent;
|
|
37304
|
-
while (ancestor && descendant !== branchRoot) {
|
|
37305
|
-
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
37306
|
-
if (ancestor.consequent === descendant) constraints.set(ancestor, true);
|
|
37307
|
-
if (ancestor.alternate === descendant) constraints.set(ancestor, false);
|
|
37308
|
-
}
|
|
37309
|
-
descendant = ancestor;
|
|
37310
|
-
ancestor = ancestor.parent;
|
|
37311
|
-
}
|
|
37312
|
-
return constraints;
|
|
37313
|
-
};
|
|
37314
|
-
const canExecuteTogether = (firstConstraints, secondConstraints) => {
|
|
37315
|
-
for (const [statement, branch] of firstConstraints) {
|
|
37316
|
-
const otherBranch = secondConstraints.get(statement);
|
|
37317
|
-
if (otherBranch !== void 0 && otherBranch !== branch) return false;
|
|
37318
|
-
}
|
|
37319
|
-
return true;
|
|
37320
|
-
};
|
|
37321
|
-
const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol, scopes) => {
|
|
37322
|
-
const assignmentConstraints = getBranchConstraints(assignmentExpression, branchRoot);
|
|
37323
|
-
const assignmentStart = getRangeStart(assignmentExpression);
|
|
37324
|
-
let hasCoExecutableWrite = false;
|
|
37325
|
-
walkAst(branchRoot, (child) => {
|
|
37326
|
-
if (hasCoExecutableWrite) return false;
|
|
37327
|
-
const childStart = getRangeStart(child);
|
|
37328
|
-
if (child === assignmentExpression || !isNodeOfType(child, "AssignmentExpression") || assignmentStart === null || childStart === null || childStart >= assignmentStart || resolveReactRefSymbol(child.left, scopes)?.id !== refSymbol.id || hasRepeatedExecutionAncestor(child, branchRoot)) return;
|
|
37329
|
-
if (canExecuteTogether(assignmentConstraints, getBranchConstraints(child, branchRoot))) {
|
|
37330
|
-
hasCoExecutableWrite = true;
|
|
37331
|
-
return false;
|
|
37332
|
-
}
|
|
37333
|
-
});
|
|
37334
|
-
return !hasCoExecutableWrite;
|
|
37335
|
-
};
|
|
37336
36625
|
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
37337
|
-
if (assignmentExpression.operator
|
|
37338
|
-
if (assignmentExpression.operator !== "=") return false;
|
|
36626
|
+
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
37339
36627
|
let descendant = assignmentExpression;
|
|
37340
36628
|
let ancestor = descendant.parent;
|
|
37341
36629
|
while (ancestor) {
|
|
37342
|
-
if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(ancestor.test, "BinaryExpression") &&
|
|
37343
|
-
"===",
|
|
37344
|
-
"==",
|
|
37345
|
-
"!==",
|
|
37346
|
-
"!="
|
|
37347
|
-
].includes(ancestor.test.operator)) {
|
|
36630
|
+
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
37348
36631
|
const { left, right } = ancestor.test;
|
|
37349
|
-
|
|
37350
|
-
const guardedBranch = ancestor.test.operator === "===" || ancestor.test.operator === "==" ? ancestor.consequent : ancestor.alternate;
|
|
37351
|
-
if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && hasNoPriorCoExecutableWrite(assignmentExpression, guardedBranch, refSymbol, scopes)) return true;
|
|
36632
|
+
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
37352
36633
|
}
|
|
37353
36634
|
descendant = ancestor;
|
|
37354
36635
|
ancestor = descendant.parent;
|
|
@@ -37922,7 +37203,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37922
37203
|
return expressionReadsStateValue(argument, stateName);
|
|
37923
37204
|
};
|
|
37924
37205
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37925
|
-
const expression =
|
|
37206
|
+
const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
|
|
37926
37207
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37927
37208
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37928
37209
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -38271,7 +37552,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
38271
37552
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
38272
37553
|
for (const statement of functionBody.body ?? []) {
|
|
38273
37554
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
38274
|
-
const effectCall =
|
|
37555
|
+
const effectCall = statement.expression;
|
|
38275
37556
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
38276
37557
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
38277
37558
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -38901,10 +38182,9 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38901
38182
|
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
38902
38183
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38903
38184
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38904
|
-
const
|
|
38905
|
-
const firstCharacter = elementName.charCodeAt(0);
|
|
38185
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
38906
38186
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38907
|
-
if (
|
|
38187
|
+
if (node.name.name.includes("-")) return;
|
|
38908
38188
|
for (const attribute of node.attributes) {
|
|
38909
38189
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38910
38190
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -39292,7 +38572,8 @@ const noUncontrolledInput = defineRule({
|
|
|
39292
38572
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
39293
38573
|
walkAst(componentBody, (child) => {
|
|
39294
38574
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
39295
|
-
|
|
38575
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
38576
|
+
const tagName = child.name.name;
|
|
39296
38577
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
39297
38578
|
const attributes = child.attributes ?? [];
|
|
39298
38579
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -39353,7 +38634,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
39353
38634
|
severity: "warn",
|
|
39354
38635
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
39355
38636
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
39356
|
-
if (
|
|
38637
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
|
|
39357
38638
|
const attributes = node.attributes ?? [];
|
|
39358
38639
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
39359
38640
|
if (!srcAttribute) return;
|
|
@@ -40568,7 +39849,7 @@ const noUnknownProperty = defineRule({
|
|
|
40568
39849
|
}
|
|
40569
39850
|
if (fileIsNonReactJsx) return;
|
|
40570
39851
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
40571
|
-
const elementType =
|
|
39852
|
+
const elementType = node.name.name;
|
|
40572
39853
|
const firstCharacter = elementType.charCodeAt(0);
|
|
40573
39854
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
40574
39855
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40746,21 +40027,20 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40746
40027
|
});
|
|
40747
40028
|
return found;
|
|
40748
40029
|
};
|
|
40749
|
-
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40750
40030
|
const isReactClassComponent = (classNode) => {
|
|
40751
40031
|
if (isEs6Component(classNode)) return true;
|
|
40752
40032
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40753
40033
|
};
|
|
40754
|
-
const findEnclosingComponent = (node
|
|
40034
|
+
const findEnclosingComponent = (node) => {
|
|
40755
40035
|
let walker = node.parent;
|
|
40756
40036
|
while (walker) {
|
|
40757
40037
|
if (isFunctionLike$1(walker)) {
|
|
40758
40038
|
const componentName = inferFunctionLikeName(walker);
|
|
40759
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40039
|
+
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
40760
40040
|
component: walker,
|
|
40761
40041
|
name: componentName
|
|
40762
40042
|
};
|
|
40763
|
-
if (!componentName &&
|
|
40043
|
+
if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40764
40044
|
component: walker,
|
|
40765
40045
|
name: null
|
|
40766
40046
|
};
|
|
@@ -40999,7 +40279,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40999
40279
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
41000
40280
|
if (settings.allowAsProps) return;
|
|
41001
40281
|
}
|
|
41002
|
-
const enclosing = findEnclosingComponent(candidateNode
|
|
40282
|
+
const enclosing = findEnclosingComponent(candidateNode);
|
|
41003
40283
|
if (!enclosing) return;
|
|
41004
40284
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
41005
40285
|
queuedReports.push({
|
|
@@ -41010,7 +40290,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
41010
40290
|
});
|
|
41011
40291
|
};
|
|
41012
40292
|
const checkFunctionLike = (node) => {
|
|
41013
|
-
if (!
|
|
40293
|
+
if (!expressionContainsJsxOrCreateElement(node)) return;
|
|
41014
40294
|
const inferredName = inferFunctionLikeName(node);
|
|
41015
40295
|
const propInfo = isComponentDeclaredInProp(node);
|
|
41016
40296
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -42270,7 +41550,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
42270
41550
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
42271
41551
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
42272
41552
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42273
|
-
const tagName =
|
|
41553
|
+
const tagName = node.name.name;
|
|
42274
41554
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42275
41555
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
42276
41556
|
if (!onDoubleClickAttribute) return;
|
|
@@ -42290,7 +41570,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
42290
41570
|
]);
|
|
42291
41571
|
const isTextLikeInput = (openingElement) => {
|
|
42292
41572
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
42293
|
-
const tagName =
|
|
41573
|
+
const tagName = openingElement.name.name;
|
|
42294
41574
|
if (tagName === "textarea") return true;
|
|
42295
41575
|
if (tagName !== "input") return false;
|
|
42296
41576
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -42447,9 +41727,8 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
42447
41727
|
]);
|
|
42448
41728
|
const collectBooleanPropBindings = (param) => {
|
|
42449
41729
|
const bindings = /* @__PURE__ */ new Set();
|
|
42450
|
-
|
|
42451
|
-
|
|
42452
|
-
for (const property of propsBinding.properties ?? []) {
|
|
41730
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
41731
|
+
for (const property of param.properties ?? []) {
|
|
42453
41732
|
if (!isNodeOfType(property, "Property")) continue;
|
|
42454
41733
|
if (property.computed) continue;
|
|
42455
41734
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42710,7 +41989,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42710
41989
|
},
|
|
42711
41990
|
JSXOpeningElement(node) {
|
|
42712
41991
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42713
|
-
const tagName =
|
|
41992
|
+
const tagName = node.name.name;
|
|
42714
41993
|
if (tagName === "dialog") return;
|
|
42715
41994
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42716
41995
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -44257,13 +43536,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
44257
43536
|
}
|
|
44258
43537
|
return null;
|
|
44259
43538
|
};
|
|
44260
|
-
const resolveHookNameFromInitializer = (initializer
|
|
43539
|
+
const resolveHookNameFromInitializer = (initializer) => {
|
|
44261
43540
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
44262
43541
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
44263
|
-
const
|
|
44264
|
-
if (
|
|
44265
|
-
if (!isNodeOfType(
|
|
44266
|
-
return resolveTanstackQueryHookName(
|
|
43542
|
+
const binding = findVariableInitializer(initializer, initializer.name);
|
|
43543
|
+
if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
|
|
43544
|
+
if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
43545
|
+
return resolveTanstackQueryHookName(binding.initializer);
|
|
44267
43546
|
};
|
|
44268
43547
|
const queryNoRestDestructuring = defineRule({
|
|
44269
43548
|
id: "query-no-rest-destructuring",
|
|
@@ -44276,7 +43555,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
44276
43555
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
44277
43556
|
if (!node.init) return;
|
|
44278
43557
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
44279
|
-
const hookName = resolveHookNameFromInitializer(node.init
|
|
43558
|
+
const hookName = resolveHookNameFromInitializer(node.init);
|
|
44280
43559
|
if (!hookName) return;
|
|
44281
43560
|
context.report({
|
|
44282
43561
|
node: node.id,
|
|
@@ -44747,7 +44026,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44747
44026
|
severity: "warn",
|
|
44748
44027
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44749
44028
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44750
|
-
if (
|
|
44029
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
44751
44030
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44752
44031
|
node,
|
|
44753
44032
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45631,7 +44910,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
|
|
|
45631
44910
|
"useParams",
|
|
45632
44911
|
"usePathname"
|
|
45633
44912
|
]);
|
|
45634
|
-
const findHookCallBindings = (componentBody
|
|
44913
|
+
const findHookCallBindings = (componentBody) => {
|
|
45635
44914
|
const bindings = [];
|
|
45636
44915
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
45637
44916
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -45642,10 +44921,8 @@ const findHookCallBindings = (componentBody, scopes) => {
|
|
|
45642
44921
|
const callee = declarator.init.callee;
|
|
45643
44922
|
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
45644
44923
|
if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
|
|
45645
|
-
const valueSymbol = scopes.symbolFor(declarator.id);
|
|
45646
|
-
if (!valueSymbol) continue;
|
|
45647
44924
|
bindings.push({
|
|
45648
|
-
|
|
44925
|
+
valueName: declarator.id.name,
|
|
45649
44926
|
hookName: callee.name,
|
|
45650
44927
|
declarator
|
|
45651
44928
|
});
|
|
@@ -45653,49 +44930,6 @@ const findHookCallBindings = (componentBody, scopes) => {
|
|
|
45653
44930
|
}
|
|
45654
44931
|
return bindings;
|
|
45655
44932
|
};
|
|
45656
|
-
const collectExactAliasSymbols = (componentBody, sourceSymbol, scopes) => {
|
|
45657
|
-
const symbols = [sourceSymbol];
|
|
45658
|
-
const symbolIds = new Set([sourceSymbol.id]);
|
|
45659
|
-
const aliasSourceIdentifiers = /* @__PURE__ */ new Set();
|
|
45660
|
-
if (!isNodeOfType(componentBody, "BlockStatement")) return {
|
|
45661
|
-
symbols,
|
|
45662
|
-
aliasSourceIdentifiers
|
|
45663
|
-
};
|
|
45664
|
-
let didFindAlias = true;
|
|
45665
|
-
while (didFindAlias) {
|
|
45666
|
-
didFindAlias = false;
|
|
45667
|
-
for (const statement of componentBody.body ?? []) {
|
|
45668
|
-
if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "const") continue;
|
|
45669
|
-
for (const declarator of statement.declarations ?? []) {
|
|
45670
|
-
let aliasIdentifier = null;
|
|
45671
|
-
let sourceIdentifier = null;
|
|
45672
|
-
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
45673
|
-
const arrayBinding = isNodeOfType(declarator.id, "ArrayPattern") ? declarator.id.elements[0] : null;
|
|
45674
|
-
const arrayValueNode = isNodeOfType(initializer, "ArrayExpression") ? initializer.elements[0] : null;
|
|
45675
|
-
const arrayValue = arrayValueNode && !isNodeOfType(arrayValueNode, "SpreadElement") ? stripParenExpression(arrayValueNode) : null;
|
|
45676
|
-
if (isNodeOfType(declarator.id, "Identifier") && isNodeOfType(initializer, "Identifier")) {
|
|
45677
|
-
aliasIdentifier = declarator.id;
|
|
45678
|
-
sourceIdentifier = initializer;
|
|
45679
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements.length === 1 && isNodeOfType(arrayBinding, "Identifier") && isNodeOfType(initializer, "ArrayExpression") && initializer.elements.length === 1 && isNodeOfType(arrayValue, "Identifier")) {
|
|
45680
|
-
aliasIdentifier = arrayBinding;
|
|
45681
|
-
sourceIdentifier = arrayValue;
|
|
45682
|
-
}
|
|
45683
|
-
if (!aliasIdentifier || !sourceIdentifier) continue;
|
|
45684
|
-
const referencedSymbol = scopes.symbolFor(sourceIdentifier);
|
|
45685
|
-
const aliasSymbol = scopes.symbolFor(aliasIdentifier);
|
|
45686
|
-
if (!referencedSymbol || !symbolIds.has(referencedSymbol.id) || !aliasSymbol || aliasSymbol.kind !== "const" || symbolIds.has(aliasSymbol.id)) continue;
|
|
45687
|
-
symbolIds.add(aliasSymbol.id);
|
|
45688
|
-
symbols.push(aliasSymbol);
|
|
45689
|
-
aliasSourceIdentifiers.add(sourceIdentifier);
|
|
45690
|
-
didFindAlias = true;
|
|
45691
|
-
}
|
|
45692
|
-
}
|
|
45693
|
-
}
|
|
45694
|
-
return {
|
|
45695
|
-
symbols,
|
|
45696
|
-
aliasSourceIdentifiers
|
|
45697
|
-
};
|
|
45698
|
-
};
|
|
45699
44933
|
const rerenderDeferReadsHook = defineRule({
|
|
45700
44934
|
id: "rerender-defer-reads-hook",
|
|
45701
44935
|
title: "URL hook value only read in handlers",
|
|
@@ -45706,13 +44940,15 @@ const rerenderDeferReadsHook = defineRule({
|
|
|
45706
44940
|
create: (context) => {
|
|
45707
44941
|
const checkComponent = (componentBody) => {
|
|
45708
44942
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
45709
|
-
const bindings = findHookCallBindings(componentBody
|
|
44943
|
+
const bindings = findHookCallBindings(componentBody);
|
|
45710
44944
|
if (bindings.length === 0) return;
|
|
45711
44945
|
const handlerBindingNames = collectHandlerBindingNames(componentBody);
|
|
45712
44946
|
for (const binding of bindings) {
|
|
45713
|
-
const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
|
|
45714
44947
|
const referenceLocations = [];
|
|
45715
|
-
|
|
44948
|
+
walkAst(componentBody, (child) => {
|
|
44949
|
+
if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
|
|
44950
|
+
if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
|
|
44951
|
+
});
|
|
45716
44952
|
if (referenceLocations.length === 0) continue;
|
|
45717
44953
|
if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
|
|
45718
44954
|
context.report({
|
|
@@ -46083,10 +45319,14 @@ const rerenderLazyStateInit = defineRule({
|
|
|
46083
45319
|
//#endregion
|
|
46084
45320
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
46085
45321
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
46086
|
-
const callbackReturnsJsx = (callback
|
|
45322
|
+
const callbackReturnsJsx = (callback) => {
|
|
46087
45323
|
if (!callback) return false;
|
|
46088
45324
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
46089
|
-
|
|
45325
|
+
const body = callback.body;
|
|
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;
|
|
46090
45330
|
};
|
|
46091
45331
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
46092
45332
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -46164,7 +45404,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
46164
45404
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
46165
45405
|
for (const declarator of stmt.declarations ?? []) {
|
|
46166
45406
|
const init = declarator.init;
|
|
46167
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0]
|
|
45407
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
46168
45408
|
memoNode = declarator;
|
|
46169
45409
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
46170
45410
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -46230,11 +45470,8 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
46230
45470
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
46231
45471
|
const bindings = /* @__PURE__ */ new Map();
|
|
46232
45472
|
const params = functionNode.params ?? [];
|
|
46233
|
-
for (const
|
|
46234
|
-
|
|
46235
|
-
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
46236
|
-
}
|
|
46237
|
-
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45473
|
+
for (const param of params) collectFromObjectPattern(param, bindings);
|
|
45474
|
+
const propsParam = params[0];
|
|
46238
45475
|
const body = functionNode.body;
|
|
46239
45476
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
46240
45477
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -46740,8 +45977,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
46740
45977
|
if (statements.length !== 1) return;
|
|
46741
45978
|
const onlyStatement = statements[0];
|
|
46742
45979
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
46743
|
-
singleAssignment =
|
|
46744
|
-
} else if (body) singleAssignment =
|
|
45980
|
+
singleAssignment = onlyStatement.expression;
|
|
45981
|
+
} else if (body) singleAssignment = body;
|
|
46745
45982
|
if (!singleAssignment) return;
|
|
46746
45983
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
46747
45984
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|
|
@@ -53661,8 +52898,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
53661
52898
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
53662
52899
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
53663
52900
|
const firstArg = node.arguments?.[0];
|
|
53664
|
-
if (!
|
|
53665
|
-
if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
|
|
52901
|
+
if (!isNodeOfType(firstArg, "ObjectExpression")) return;
|
|
53666
52902
|
context.report({
|
|
53667
52903
|
node,
|
|
53668
52904
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|
|
@@ -54349,6 +53585,10 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
54349
53585
|
}
|
|
54350
53586
|
return false;
|
|
54351
53587
|
};
|
|
53588
|
+
const getJsxOpeningElementName = (node) => {
|
|
53589
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53590
|
+
return null;
|
|
53591
|
+
};
|
|
54352
53592
|
const stylePropObject = defineRule({
|
|
54353
53593
|
id: "style-prop-object",
|
|
54354
53594
|
title: "Style prop is not an object",
|
|
@@ -54360,8 +53600,7 @@ const stylePropObject = defineRule({
|
|
|
54360
53600
|
const allowSet = new Set(allow);
|
|
54361
53601
|
return {
|
|
54362
53602
|
JSXOpeningElement(node) {
|
|
54363
|
-
|
|
54364
|
-
const elementName = resolveJsxElementType(node);
|
|
53603
|
+
const elementName = getJsxOpeningElementName(node);
|
|
54365
53604
|
if (elementName && allowSet.has(elementName)) return;
|
|
54366
53605
|
if (elementName) {
|
|
54367
53606
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -55066,7 +54305,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
55066
54305
|
create: (context) => {
|
|
55067
54306
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
55068
54307
|
return { JSXOpeningElement(node) {
|
|
55069
|
-
if (
|
|
54308
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
55070
54309
|
const attributes = node.attributes ?? [];
|
|
55071
54310
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
55072
54311
|
if (!hrefAttribute?.value) return;
|
|
@@ -55687,7 +54926,8 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
55687
54926
|
create: (context) => ({
|
|
55688
54927
|
JSXElement(node) {
|
|
55689
54928
|
const openingElement = node.openingElement;
|
|
55690
|
-
|
|
54929
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
|
|
54930
|
+
const tagName = openingElement.name.name;
|
|
55691
54931
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
55692
54932
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
55693
54933
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|