oxlint-plugin-react-doctor 0.7.5-dev.dbd4067 → 0.7.6-dev.0150fc9
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 +719 -214
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1518,6 +1518,37 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1518
1518
|
}
|
|
1519
1519
|
};
|
|
1520
1520
|
//#endregion
|
|
1521
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
1522
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/plugin/utils/resolve-jsx-element-type.ts
|
|
1525
|
+
const resolveConstantStringBinding = (name) => {
|
|
1526
|
+
const binding = findVariableInitializer(name, name.name);
|
|
1527
|
+
if (!binding?.initializer) return null;
|
|
1528
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
1529
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
1530
|
+
if (declarator.id !== binding.bindingIdentifier) return null;
|
|
1531
|
+
const declaration = declarator.parent;
|
|
1532
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
1533
|
+
if (declaration.kind !== "const") return null;
|
|
1534
|
+
const initializer = stripParenExpression(binding.initializer);
|
|
1535
|
+
return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
1536
|
+
};
|
|
1537
|
+
const flattenJsxName$2 = (name) => {
|
|
1538
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1539
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1540
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1541
|
+
return "";
|
|
1542
|
+
};
|
|
1543
|
+
const resolveJsxElementType = (openingElement) => {
|
|
1544
|
+
const name = openingElement.name;
|
|
1545
|
+
if (isNodeOfType(name, "JSXIdentifier")) {
|
|
1546
|
+
if (!isUppercaseName(name.name)) return name.name;
|
|
1547
|
+
return resolveConstantStringBinding(name) ?? name.name;
|
|
1548
|
+
}
|
|
1549
|
+
return flattenJsxName$2(name);
|
|
1550
|
+
};
|
|
1551
|
+
//#endregion
|
|
1521
1552
|
//#region src/plugin/utils/get-element-type.ts
|
|
1522
1553
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1523
1554
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1530,14 +1561,8 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1530
1561
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1531
1562
|
return a11ySettings;
|
|
1532
1563
|
};
|
|
1533
|
-
const flattenJsxName$2 = (name) => {
|
|
1534
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1535
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1536
|
-
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1537
|
-
return "";
|
|
1538
|
-
};
|
|
1539
1564
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1540
|
-
const baseName =
|
|
1565
|
+
const baseName = resolveJsxElementType(openingElement);
|
|
1541
1566
|
if (a11ySettings.polymorphicPropName) {
|
|
1542
1567
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1543
1568
|
if (polymorphicAttribute) {
|
|
@@ -2372,8 +2397,9 @@ const anchorIsValid = defineRule({
|
|
|
2372
2397
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2373
2398
|
return { JSXOpeningElement(node) {
|
|
2374
2399
|
if (isTestlikeFile) return;
|
|
2375
|
-
|
|
2376
|
-
if (
|
|
2400
|
+
const tag = getElementType(node, context.settings);
|
|
2401
|
+
if (!fileHasJsxA11ySettings && tag !== "a") return;
|
|
2402
|
+
if (tag !== "a") return;
|
|
2377
2403
|
let hrefAttribute;
|
|
2378
2404
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2379
2405
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5728,7 +5754,7 @@ const buttonHasType = defineRule({
|
|
|
5728
5754
|
return {
|
|
5729
5755
|
JSXOpeningElement(node) {
|
|
5730
5756
|
if (isTestlikeFile) return;
|
|
5731
|
-
if (
|
|
5757
|
+
if (resolveJsxElementType(node) !== "button") return;
|
|
5732
5758
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5733
5759
|
if (!typeAttr) {
|
|
5734
5760
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5918,7 +5944,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5918
5944
|
};
|
|
5919
5945
|
return {
|
|
5920
5946
|
JSXOpeningElement(node) {
|
|
5921
|
-
if (
|
|
5947
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
5922
5948
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5923
5949
|
},
|
|
5924
5950
|
CallExpression(node) {
|
|
@@ -5995,7 +6021,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5995
6021
|
//#endregion
|
|
5996
6022
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
5997
6023
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
5998
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6024
|
+
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
5999
6025
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6000
6026
|
let symbol = scopes.symbolFor(identifier);
|
|
6001
6027
|
while (symbol?.kind === "const") {
|
|
@@ -6270,6 +6296,131 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6270
6296
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6271
6297
|
};
|
|
6272
6298
|
//#endregion
|
|
6299
|
+
//#region src/plugin/utils/collect-safely-validated-local-storage-keys.ts
|
|
6300
|
+
const isLocalStorageCall = (node, methodName) => {
|
|
6301
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6302
|
+
const callee = stripParenExpression(node.callee);
|
|
6303
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
6304
|
+
const receiver = stripParenExpression(callee.object);
|
|
6305
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "localStorage" && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6306
|
+
};
|
|
6307
|
+
const catchReturnsFallback = (tryStatement) => {
|
|
6308
|
+
const handler = tryStatement.handler;
|
|
6309
|
+
if (!handler) return false;
|
|
6310
|
+
let hasReturn = false;
|
|
6311
|
+
let hasThrow = false;
|
|
6312
|
+
walkAst(handler.body, (child) => {
|
|
6313
|
+
if (isFunctionLike$1(child)) return false;
|
|
6314
|
+
if (isNodeOfType(child, "ReturnStatement")) hasReturn = true;
|
|
6315
|
+
if (isNodeOfType(child, "ThrowStatement")) hasThrow = true;
|
|
6316
|
+
});
|
|
6317
|
+
return hasReturn && !hasThrow;
|
|
6318
|
+
};
|
|
6319
|
+
const resolveValidatorFunction = (callee, scopes) => {
|
|
6320
|
+
const unwrappedCallee = stripParenExpression(callee);
|
|
6321
|
+
if (!isNodeOfType(unwrappedCallee, "Identifier")) return null;
|
|
6322
|
+
const symbol = scopes.symbolFor(unwrappedCallee);
|
|
6323
|
+
if (!symbol) return null;
|
|
6324
|
+
const candidate = symbol.kind === "function" ? symbol.declarationNode : symbol.initializer;
|
|
6325
|
+
return isFunctionLike$1(candidate) ? candidate : null;
|
|
6326
|
+
};
|
|
6327
|
+
const validatorChecksPayloadProperties = (validatorFunction, scopes) => {
|
|
6328
|
+
if (!isFunctionLike$1(validatorFunction)) return false;
|
|
6329
|
+
const firstParameter = validatorFunction.params?.[0];
|
|
6330
|
+
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
6331
|
+
const parameterSymbol = scopes.symbolFor(firstParameter);
|
|
6332
|
+
if (!parameterSymbol) return false;
|
|
6333
|
+
const payloadSymbolIds = new Set([parameterSymbol.id]);
|
|
6334
|
+
let hasPropertyTypeCheck = false;
|
|
6335
|
+
walkAst(validatorFunction.body, (child) => {
|
|
6336
|
+
if (isFunctionLike$1(child)) return false;
|
|
6337
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.init) {
|
|
6338
|
+
const initializer = stripParenExpression(child.init);
|
|
6339
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
6340
|
+
const initializerSymbol = scopes.symbolFor(initializer);
|
|
6341
|
+
if (initializerSymbol && payloadSymbolIds.has(initializerSymbol.id)) {
|
|
6342
|
+
const aliasSymbol = scopes.symbolFor(child.id);
|
|
6343
|
+
if (aliasSymbol) payloadSymbolIds.add(aliasSymbol.id);
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
if (!isNodeOfType(child, "UnaryExpression") || child.operator !== "typeof") return;
|
|
6348
|
+
const checkedValue = stripParenExpression(child.argument);
|
|
6349
|
+
if (!isNodeOfType(checkedValue, "MemberExpression")) return;
|
|
6350
|
+
const receiver = stripParenExpression(checkedValue.object);
|
|
6351
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6352
|
+
const receiverSymbol = scopes.symbolFor(receiver);
|
|
6353
|
+
if (receiverSymbol && payloadSymbolIds.has(receiverSymbol.id)) hasPropertyTypeCheck = true;
|
|
6354
|
+
});
|
|
6355
|
+
return hasPropertyTypeCheck;
|
|
6356
|
+
};
|
|
6357
|
+
const incrementCount = (counts, keyValue) => {
|
|
6358
|
+
counts.set(keyValue, (counts.get(keyValue) ?? 0) + 1);
|
|
6359
|
+
};
|
|
6360
|
+
const isReturnedExpression = (expression) => {
|
|
6361
|
+
const parent = expression.parent;
|
|
6362
|
+
return Boolean(parent && isNodeOfType(parent, "ReturnStatement") && parent.argument === expression);
|
|
6363
|
+
};
|
|
6364
|
+
const collectSafelyValidatedLocalStorageKeys = ({ programNode, scopes, resolveKey }) => {
|
|
6365
|
+
const readCountsByKey = /* @__PURE__ */ new Map();
|
|
6366
|
+
const safeReadCountsByKey = /* @__PURE__ */ new Map();
|
|
6367
|
+
const safeReadSymbolIds = /* @__PURE__ */ new Set();
|
|
6368
|
+
walkAst(programNode, (child) => {
|
|
6369
|
+
if (!isNodeOfType(child, "CallExpression") || !isLocalStorageCall(child, "getItem")) return;
|
|
6370
|
+
const keyArgument = child.arguments?.[0];
|
|
6371
|
+
if (!keyArgument) return;
|
|
6372
|
+
const keyValue = resolveKey(keyArgument);
|
|
6373
|
+
if (keyValue !== null) incrementCount(readCountsByKey, keyValue);
|
|
6374
|
+
});
|
|
6375
|
+
walkAst(programNode, (child) => {
|
|
6376
|
+
if (!isNodeOfType(child, "TryStatement") || !catchReturnsFallback(child)) return;
|
|
6377
|
+
const rawValueKeys = /* @__PURE__ */ new Map();
|
|
6378
|
+
const parsedValueSources = /* @__PURE__ */ new Map();
|
|
6379
|
+
walkAst(child.block, (tryChild) => {
|
|
6380
|
+
if (isFunctionLike$1(tryChild)) return false;
|
|
6381
|
+
if (isNodeOfType(tryChild, "VariableDeclarator") && isNodeOfType(tryChild.id, "Identifier") && tryChild.init) {
|
|
6382
|
+
const initializer = stripParenExpression(tryChild.init);
|
|
6383
|
+
if (isNodeOfType(initializer, "CallExpression") && isLocalStorageCall(initializer, "getItem")) {
|
|
6384
|
+
const keyArgument = initializer.arguments?.[0];
|
|
6385
|
+
const bindingSymbol = scopes.symbolFor(tryChild.id);
|
|
6386
|
+
if (keyArgument && bindingSymbol) {
|
|
6387
|
+
const keyValue = resolveKey(keyArgument);
|
|
6388
|
+
if (keyValue !== null) rawValueKeys.set(bindingSymbol.id, keyValue);
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
if (isNodeOfType(initializer, "CallExpression") && isGlobalMethodCall(initializer, "JSON", "parse")) {
|
|
6392
|
+
const rawValueArgument = initializer.arguments?.[0];
|
|
6393
|
+
const parsedValueSymbol = scopes.symbolFor(tryChild.id);
|
|
6394
|
+
if (rawValueArgument && isNodeOfType(rawValueArgument, "Identifier") && parsedValueSymbol) {
|
|
6395
|
+
const rawValueSymbol = scopes.symbolFor(rawValueArgument);
|
|
6396
|
+
if (rawValueSymbol && rawValueKeys.has(rawValueSymbol.id)) parsedValueSources.set(parsedValueSymbol.id, rawValueSymbol.id);
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
if (!isNodeOfType(tryChild, "ConditionalExpression") || !isReturnedExpression(tryChild)) return;
|
|
6401
|
+
const test = stripParenExpression(tryChild.test);
|
|
6402
|
+
if (!isNodeOfType(test, "CallExpression")) return;
|
|
6403
|
+
const testedValue = test.arguments?.[0];
|
|
6404
|
+
if (!testedValue || !isNodeOfType(testedValue, "Identifier")) return;
|
|
6405
|
+
const parsedValueSymbol = scopes.symbolFor(testedValue);
|
|
6406
|
+
if (!parsedValueSymbol) return;
|
|
6407
|
+
const rawValueSymbolId = parsedValueSources.get(parsedValueSymbol.id);
|
|
6408
|
+
if (rawValueSymbolId === void 0) return;
|
|
6409
|
+
const returnedValue = stripParenExpression(tryChild.consequent);
|
|
6410
|
+
if (!isNodeOfType(returnedValue, "Identifier") || scopes.symbolFor(returnedValue)?.id !== parsedValueSymbol.id) return;
|
|
6411
|
+
const validatorFunction = resolveValidatorFunction(test.callee, scopes);
|
|
6412
|
+
if (!validatorFunction || !validatorChecksPayloadProperties(validatorFunction, scopes)) return;
|
|
6413
|
+
const keyValue = rawValueKeys.get(rawValueSymbolId);
|
|
6414
|
+
if (keyValue === void 0 || safeReadSymbolIds.has(rawValueSymbolId)) return;
|
|
6415
|
+
safeReadSymbolIds.add(rawValueSymbolId);
|
|
6416
|
+
incrementCount(safeReadCountsByKey, keyValue);
|
|
6417
|
+
});
|
|
6418
|
+
});
|
|
6419
|
+
const safeKeys = /* @__PURE__ */ new Set();
|
|
6420
|
+
for (const [keyValue, readCount] of readCountsByKey) if (safeReadCountsByKey.get(keyValue) === readCount) safeKeys.add(keyValue);
|
|
6421
|
+
return safeKeys;
|
|
6422
|
+
};
|
|
6423
|
+
//#endregion
|
|
6273
6424
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6274
6425
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6275
6426
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6291,26 +6442,39 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6291
6442
|
severity: "warn",
|
|
6292
6443
|
category: "Correctness",
|
|
6293
6444
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6294
|
-
create: (context) =>
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6445
|
+
create: (context) => {
|
|
6446
|
+
let safelyValidatedStorageKeys = /* @__PURE__ */ new Set();
|
|
6447
|
+
return {
|
|
6448
|
+
Program(node) {
|
|
6449
|
+
safelyValidatedStorageKeys = collectSafelyValidatedLocalStorageKeys({
|
|
6450
|
+
programNode: node,
|
|
6451
|
+
scopes: context.scopes,
|
|
6452
|
+
resolveKey: (keyNode) => resolveStringKey(keyNode, context)
|
|
6453
|
+
});
|
|
6454
|
+
},
|
|
6455
|
+
CallExpression(node) {
|
|
6456
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6457
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6458
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6459
|
+
if (receiver.name !== "localStorage") return;
|
|
6460
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6461
|
+
if (node.callee.property.name !== "setItem") return;
|
|
6462
|
+
const keyArg = node.arguments?.[0];
|
|
6463
|
+
if (!keyArg) return;
|
|
6464
|
+
const keyValue = resolveStringKey(keyArg, context);
|
|
6465
|
+
if (keyValue === null) return;
|
|
6466
|
+
if (isVersionedKey(keyValue)) return;
|
|
6467
|
+
if (safelyValidatedStorageKeys.has(keyValue)) return;
|
|
6468
|
+
const valueArg = node.arguments?.[1];
|
|
6469
|
+
if (!valueArg) return;
|
|
6470
|
+
if (!isJsonStringifyCall(valueArg)) return;
|
|
6471
|
+
context.report({
|
|
6472
|
+
node: keyArg,
|
|
6473
|
+
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").`
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
6476
|
+
};
|
|
6477
|
+
}
|
|
6314
6478
|
});
|
|
6315
6479
|
//#endregion
|
|
6316
6480
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -6929,6 +7093,30 @@ const corsCookieTrustRisk = defineRule({
|
|
|
6929
7093
|
})
|
|
6930
7094
|
});
|
|
6931
7095
|
//#endregion
|
|
7096
|
+
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
7097
|
+
const findMatchingBracket = (content, openIndex) => {
|
|
7098
|
+
const open = content[openIndex];
|
|
7099
|
+
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
7100
|
+
if (close === "") return -1;
|
|
7101
|
+
let depth = 0;
|
|
7102
|
+
let stringDelimiter = null;
|
|
7103
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
7104
|
+
const character = content[index];
|
|
7105
|
+
if (stringDelimiter !== null) {
|
|
7106
|
+
if (character === "\\") index += 1;
|
|
7107
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
7108
|
+
continue;
|
|
7109
|
+
}
|
|
7110
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
7111
|
+
else if (character === open) depth += 1;
|
|
7112
|
+
else if (character === close) {
|
|
7113
|
+
depth -= 1;
|
|
7114
|
+
if (depth === 0) return index;
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
return -1;
|
|
7118
|
+
};
|
|
7119
|
+
//#endregion
|
|
6932
7120
|
//#region src/plugin/rules/security-scan/dangerous-html-sink.ts
|
|
6933
7121
|
const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
|
|
6934
7122
|
const HTML_VALUE_START_PATTERN = /(?:__html\s*:|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
|
|
@@ -6944,6 +7132,7 @@ const I18N_VALUE_PATTERN = /\b(?:t|i18n|translate|formatMessage|intl)\s*[.(]/;
|
|
|
6944
7132
|
const ESCAPING_SERIALIZER_CALL_PATTERN = /^(?:[\w$.]+\.)?(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*)\s*\(/;
|
|
6945
7133
|
const HIGHLIGHTER_LIBRARY_PATTERN = /\b(?:shiki|prism|hljs|highlightjs|getHighlighter|codeToHtml|codeToHast|refractor|lowlight|starry-night)\b|highlight\.js/i;
|
|
6946
7134
|
const SERIALIZER_ASSIGNMENT_PATTERN = /[:=]\s*[^\n;]*(?:\b(?:katex|shiki|hljs|prism|mermaid)\b|hast-util-to-html|renderHtmlFromRichText|(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast)\s*\()/i;
|
|
7135
|
+
const SERIALIZER_CALL_PROVENANCE_PATTERN = /\b(?:(?:katex|shiki|hljs|prism|mermaid|highlighter)[\w$]*\.(?:render\w*|highlight\w*|codeTo(?:Html|Hast))|(?:toHtml|render(?:Html|HTML)[A-Za-z]*|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*))\s*\(/i;
|
|
6947
7136
|
const BARE_IDENTIFIER_VALUE_PATTERN = /^[\w$]+\s*(?:[;,})\n]|$)/;
|
|
6948
7137
|
const IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN = /^[\w$]+\s*(?:\|\||\?\?)\s*(?:"[^"\n]*"|'[^'\n]*'|`[^`$]*`)\s*(?:[;,})\n]|$)/;
|
|
6949
7138
|
const MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN = /^[\w$]+(?:\.[\w$]+|\[[^\]]*\])+\s*(?:[;,})\n]|$)/;
|
|
@@ -7103,22 +7292,66 @@ const findContainingBlockEndIndex = (fileContent, targetIndex) => {
|
|
|
7103
7292
|
return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
|
|
7104
7293
|
};
|
|
7105
7294
|
const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
|
|
7106
|
-
const initializerPattern = new RegExp(`(
|
|
7295
|
+
const initializerPattern = new RegExp(`(const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
|
|
7107
7296
|
let nearestDeclaration = null;
|
|
7108
7297
|
let nearestDeclarationIndex = -1;
|
|
7109
7298
|
for (const match of fileContent.matchAll(initializerPattern)) {
|
|
7110
7299
|
const declarationIndex = match.index;
|
|
7111
7300
|
if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
|
|
7112
7301
|
nearestDeclarationIndex = declarationIndex;
|
|
7113
|
-
const initializer = match[
|
|
7302
|
+
const initializer = match[2];
|
|
7114
7303
|
if (initializer === void 0) continue;
|
|
7115
7304
|
nearestDeclaration = {
|
|
7305
|
+
declarationIndex,
|
|
7116
7306
|
initializer,
|
|
7117
|
-
initializerStartIndex: declarationIndex + match[0].length - initializer.length
|
|
7307
|
+
initializerStartIndex: declarationIndex + match[0].length - initializer.length,
|
|
7308
|
+
isImmutable: match[1] === "const"
|
|
7118
7309
|
};
|
|
7119
7310
|
}
|
|
7120
7311
|
return nearestDeclaration;
|
|
7121
7312
|
};
|
|
7313
|
+
const isDeclarationStable = (identifier, declaration, usageIndex, fileContent) => {
|
|
7314
|
+
if (declaration.isImmutable) return true;
|
|
7315
|
+
const textAfterInitializer = fileContent.slice(declaration.initializerStartIndex + declaration.initializer.length, usageIndex);
|
|
7316
|
+
return !new RegExp(`(?:^|[^\\w$.])${escapeRegExp(identifier)}\\s*=(?!=)`).test(textAfterInitializer);
|
|
7317
|
+
};
|
|
7318
|
+
const isTrustedHighlighterValue = (valueExpression, fileContent, sinkIndex) => {
|
|
7319
|
+
if (!/highlight/i.test(valueExpression)) return false;
|
|
7320
|
+
const identifier = valueExpression.match(/^([\w$]+)/)?.[1];
|
|
7321
|
+
if (identifier === void 0) return false;
|
|
7322
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7323
|
+
if (declaration !== null) return isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(declaration.initializer);
|
|
7324
|
+
return /highlighted/i.test(valueExpression) || HIGHLIGHTER_LIBRARY_PATTERN.test(fileContent);
|
|
7325
|
+
};
|
|
7326
|
+
const isExplicitlyTrustedHtmlValue = (valueExpression, fileContent, sinkIndex, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7327
|
+
const trimmedExpression = valueExpression.trim();
|
|
7328
|
+
const concatenatedParts = splitTopLevelByPlus(trimmedExpression);
|
|
7329
|
+
if (concatenatedParts.length > 1) return concatenatedParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
|
|
7330
|
+
const interpolationParts = [...trimmedExpression.matchAll(/\$\{([^}]*)\}/g)].flatMap((match) => match[1] === void 0 ? [] : [match[1]]);
|
|
7331
|
+
if (interpolationParts.length > 1) return interpolationParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
|
|
7332
|
+
const identifier = trimmedExpression.match(/^([\w$]+)(?:(?:\??\.[\w$]+)|(?:\[\s*(?:"[^"\n]*"|'[^'\n]*'|\d+)\s*\]))*$/)?.[1];
|
|
7333
|
+
if (identifier && !visitedIdentifiers.has(identifier)) {
|
|
7334
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7335
|
+
if (declaration && isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) {
|
|
7336
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7337
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7338
|
+
return isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex, nextVisitedIdentifiers);
|
|
7339
|
+
}
|
|
7340
|
+
}
|
|
7341
|
+
if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`) || MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`) || SANITIZER_PATTERN.test(trimmedExpression) || ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression) || I18N_VALUE_PATTERN.test(trimmedExpression) || ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression) || isTrustedHighlighterValue(trimmedExpression, fileContent, sinkIndex)) return true;
|
|
7342
|
+
return false;
|
|
7343
|
+
};
|
|
7344
|
+
const doesExpressionAliasIdentifier = (expression, targetIdentifier, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7345
|
+
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
7346
|
+
if (!identifier) return false;
|
|
7347
|
+
if (identifier === targetIdentifier) return true;
|
|
7348
|
+
if (visitedIdentifiers.has(identifier)) return false;
|
|
7349
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
|
|
7350
|
+
if (!declaration?.isImmutable) return false;
|
|
7351
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7352
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7353
|
+
return doesExpressionAliasIdentifier(declaration.initializer, targetIdentifier, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
|
|
7354
|
+
};
|
|
7122
7355
|
const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
|
|
7123
7356
|
const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
|
|
7124
7357
|
let closestSource = null;
|
|
@@ -7126,12 +7359,16 @@ const findContainingFunctionParameterSource = (identifier, sinkIndex, fileConten
|
|
|
7126
7359
|
for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
|
|
7127
7360
|
const matchIndex = match.index;
|
|
7128
7361
|
if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
|
|
7129
|
-
|
|
7362
|
+
const bodyEndIndex = findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{"));
|
|
7363
|
+
if (bodyEndIndex < sinkIndex) continue;
|
|
7130
7364
|
const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
|
|
7131
7365
|
if (parameterIndex < 0) continue;
|
|
7132
7366
|
closestStartIndex = matchIndex;
|
|
7367
|
+
const functionName = match[1] ?? "";
|
|
7133
7368
|
closestSource = {
|
|
7134
|
-
|
|
7369
|
+
bodyEndIndex,
|
|
7370
|
+
declarationNameIndex: matchIndex + match[0].indexOf(functionName),
|
|
7371
|
+
functionName,
|
|
7135
7372
|
parameterIndex
|
|
7136
7373
|
};
|
|
7137
7374
|
}
|
|
@@ -7162,28 +7399,56 @@ const splitTopLevelArguments = (argumentText) => {
|
|
|
7162
7399
|
argumentsList.push(argumentText.slice(startIndex).trim());
|
|
7163
7400
|
return argumentsList;
|
|
7164
7401
|
};
|
|
7165
|
-
const
|
|
7166
|
-
const
|
|
7167
|
-
if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
|
|
7168
|
-
if (MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
|
|
7169
|
-
if (SANITIZER_PATTERN.test(trimmedExpression)) return false;
|
|
7170
|
-
if (ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression)) return false;
|
|
7171
|
-
if (I18N_VALUE_PATTERN.test(trimmedExpression)) return false;
|
|
7172
|
-
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression)) return false;
|
|
7173
|
-
if (HTML_TAINT_PATTERN.test(trimmedExpression)) return true;
|
|
7174
|
-
const identifier = trimmedExpression.match(/^([\w$]+)\s*(?:[;,})\n]|$)/)?.[1];
|
|
7402
|
+
const isBackedByFunctionParameter = (expression, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7403
|
+
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
7175
7404
|
if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
|
|
7405
|
+
if (findContainingFunctionParameterSource(identifier, expressionIndex, fileContent) !== null) return true;
|
|
7406
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
|
|
7407
|
+
if (!declaration || !isDeclarationStable(identifier, declaration, expressionIndex, fileContent)) return false;
|
|
7408
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7409
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7410
|
+
return isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
|
|
7411
|
+
};
|
|
7412
|
+
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers, visitedCallSites) => {
|
|
7413
|
+
const trimmedExpression = expression.trim();
|
|
7414
|
+
if (isExplicitlyTrustedHtmlValue(trimmedExpression, fileContent, sinkIndex)) return false;
|
|
7415
|
+
const identifier = trimmedExpression.match(/^([\w$]+)(?:\.|\s*(?:[;,})\n]|$))/)?.[1];
|
|
7416
|
+
if (identifier === void 0) return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7417
|
+
if (visitedIdentifiers.has(identifier)) return false;
|
|
7176
7418
|
visitedIdentifiers.add(identifier);
|
|
7177
7419
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7178
|
-
if (declaration !== null && isHtmlTainted(declaration.initializer, fileContent, sinkIndex, visitedIdentifiers)) return true;
|
|
7179
7420
|
const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
|
|
7180
|
-
if (parameterSource === null ||
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
if (
|
|
7185
|
-
|
|
7186
|
-
|
|
7421
|
+
if (declaration !== null && (parameterSource === null || declaration.declarationIndex > parameterSource.declarationNameIndex)) {
|
|
7422
|
+
if (isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex)) return false;
|
|
7423
|
+
if (!isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) return true;
|
|
7424
|
+
if (isHtmlTainted(declaration.initializer, fileContent, declaration.initializerStartIndex, visitedIdentifiers, visitedCallSites)) return true;
|
|
7425
|
+
if (isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent)) return false;
|
|
7426
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7427
|
+
}
|
|
7428
|
+
if (parameterSource !== null && parameterSource.functionName.length > 0) {
|
|
7429
|
+
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(`, "g");
|
|
7430
|
+
let didInspectCallArgument = false;
|
|
7431
|
+
let didInspectOnlyExplicitlyTrustedArguments = true;
|
|
7432
|
+
for (const callMatch of fileContent.matchAll(callPattern)) {
|
|
7433
|
+
if (callMatch.index === parameterSource.declarationNameIndex || visitedCallSites.has(callMatch.index)) continue;
|
|
7434
|
+
const openingParenthesisIndex = callMatch.index + callMatch[0].lastIndexOf("(");
|
|
7435
|
+
const closingParenthesisIndex = findMatchingBracket(fileContent, openingParenthesisIndex);
|
|
7436
|
+
if (closingParenthesisIndex < 0) continue;
|
|
7437
|
+
const argument = splitTopLevelArguments(fileContent.slice(openingParenthesisIndex + 1, closingParenthesisIndex))[parameterSource.parameterIndex];
|
|
7438
|
+
if (argument === void 0) continue;
|
|
7439
|
+
if (callMatch.index >= parameterSource.declarationNameIndex && callMatch.index <= parameterSource.bodyEndIndex && doesExpressionAliasIdentifier(argument, identifier, callMatch.index, fileContent)) continue;
|
|
7440
|
+
didInspectCallArgument = true;
|
|
7441
|
+
didInspectOnlyExplicitlyTrustedArguments &&= isExplicitlyTrustedHtmlValue(argument, fileContent, callMatch.index);
|
|
7442
|
+
const callVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7443
|
+
callVisitedIdentifiers.delete(identifier);
|
|
7444
|
+
const nextVisitedCallSites = new Set(visitedCallSites);
|
|
7445
|
+
nextVisitedCallSites.add(callMatch.index);
|
|
7446
|
+
if (isHtmlTainted(argument, fileContent, callMatch.index, callVisitedIdentifiers, nextVisitedCallSites)) return true;
|
|
7447
|
+
}
|
|
7448
|
+
if (didInspectCallArgument) return !didInspectOnlyExplicitlyTrustedArguments && HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7449
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7450
|
+
}
|
|
7451
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7187
7452
|
};
|
|
7188
7453
|
const dangerousHtmlSink = defineRule({
|
|
7189
7454
|
id: "dangerous-html-sink",
|
|
@@ -7222,7 +7487,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
7222
7487
|
const longValueTail = HTML_VALUE_START_PATTERN.exec(lines.slice(lineIndex, lineIndex + 1 + STATIC_TEMPLATE_LOOKAHEAD_LINES).join("\n"))?.[1]?.trimStart();
|
|
7223
7488
|
let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
|
|
7224
7489
|
const valueIdentifier = BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) || MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN.test(valueExpression) || IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN.test(valueExpression) ? valueExpression.match(/^[\w$]+/)?.[0] : void 0;
|
|
7225
|
-
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
|
|
7490
|
+
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) && findContainingFunctionParameterSource(valueIdentifier, sinkIndex, file.content) === null) {
|
|
7226
7491
|
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
|
|
7227
7492
|
if (declarationInitializer !== null) {
|
|
7228
7493
|
if (isPureStringLiteralConcat(declarationInitializer)) continue;
|
|
@@ -7231,17 +7496,23 @@ const dangerousHtmlSink = defineRule({
|
|
|
7231
7496
|
}
|
|
7232
7497
|
if (templateInterpolations === "") continue;
|
|
7233
7498
|
const judgedExpression = templateInterpolations ?? valueExpression;
|
|
7234
|
-
|
|
7235
|
-
if (
|
|
7236
|
-
if (
|
|
7237
|
-
if (!
|
|
7499
|
+
const doesJudgedExpressionCombineValues = splitTopLevelByPlus(judgedExpression).length > 1 || (templateInterpolations?.match(/\$\{/g)?.length ?? 0) > 1;
|
|
7500
|
+
if (!doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
7501
|
+
if (!doesJudgedExpressionCombineValues && ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7502
|
+
if (!doesJudgedExpressionCombineValues && I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7503
|
+
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
|
|
7238
7504
|
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
7239
|
-
if (
|
|
7240
|
-
if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
7505
|
+
if (isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
|
|
7241
7506
|
if (valueIdentifier !== void 0) {
|
|
7242
7507
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
7243
|
-
|
|
7244
|
-
|
|
7508
|
+
const visibleDeclaration = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content);
|
|
7509
|
+
const visibleInitializer = visibleDeclaration?.initializer;
|
|
7510
|
+
if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
|
|
7511
|
+
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7512
|
+
if (visibleInitializer === void 0 ? fromSerializer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer)) continue;
|
|
7513
|
+
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7514
|
+
if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`)) continue;
|
|
7515
|
+
}
|
|
7245
7516
|
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
7246
7517
|
if (new RegExp(`highlight[\\w$]*\\s*\\.map\\(\\s*(?:async\\s+)?\\(?\\s*${escapedIdentifier}\\b`, "i").test(file.content) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
7247
7518
|
}
|
|
@@ -7326,7 +7597,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7326
7597
|
const getOpeningElementTagName = (openingElement) => {
|
|
7327
7598
|
if (!openingElement) return null;
|
|
7328
7599
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7329
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement
|
|
7600
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return resolveJsxElementType(openingElement);
|
|
7330
7601
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7331
7602
|
let cursor = openingElement.name;
|
|
7332
7603
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7578,7 +7849,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7578
7849
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7579
7850
|
return { JSXOpeningElement(node) {
|
|
7580
7851
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7581
|
-
const tagName = node
|
|
7852
|
+
const tagName = resolveJsxElementType(node);
|
|
7582
7853
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7583
7854
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7584
7855
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -9070,6 +9341,30 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
|
|
|
9070
9341
|
}
|
|
9071
9342
|
return matchingBlocks.size > 0;
|
|
9072
9343
|
};
|
|
9344
|
+
const doMatchingNodesCoverEveryPathFromFunctionEntry = (owner, matchingNodes, context) => {
|
|
9345
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
9346
|
+
if (!functionCfg) return false;
|
|
9347
|
+
const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
|
|
9348
|
+
if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
|
|
9349
|
+
const matchingBlock = functionCfg.blockOf(matchingNode);
|
|
9350
|
+
return matchingBlock ? [matchingBlock] : [];
|
|
9351
|
+
}));
|
|
9352
|
+
if (matchingBlocks.size === 0) return false;
|
|
9353
|
+
const visitedBlocks = new Set([functionCfg.entry]);
|
|
9354
|
+
const pendingBlocks = [functionCfg.entry];
|
|
9355
|
+
while (pendingBlocks.length > 0) {
|
|
9356
|
+
const currentBlock = pendingBlocks.pop();
|
|
9357
|
+
if (!currentBlock) break;
|
|
9358
|
+
if (matchingBlocks.has(currentBlock)) continue;
|
|
9359
|
+
for (const edge of currentBlock.successors) {
|
|
9360
|
+
if (edge.to === functionCfg.exit) return false;
|
|
9361
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
9362
|
+
visitedBlocks.add(edge.to);
|
|
9363
|
+
pendingBlocks.push(edge.to);
|
|
9364
|
+
}
|
|
9365
|
+
}
|
|
9366
|
+
return true;
|
|
9367
|
+
};
|
|
9073
9368
|
const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
9074
9369
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
9075
9370
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
@@ -9270,6 +9565,83 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
9270
9565
|
});
|
|
9271
9566
|
return didCleanupFunctionMatch;
|
|
9272
9567
|
};
|
|
9568
|
+
const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
|
|
9569
|
+
const callExpression = stripParenExpression(expression);
|
|
9570
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return false;
|
|
9571
|
+
const bindCallee = stripParenExpression(callExpression.callee);
|
|
9572
|
+
if (!isNodeOfType(bindCallee, "MemberExpression") || bindCallee.computed || !isNodeOfType(bindCallee.property, "Identifier") || bindCallee.property.name !== "bind") return false;
|
|
9573
|
+
const releaseMember = stripParenExpression(bindCallee.object);
|
|
9574
|
+
if (!isNodeOfType(releaseMember, "MemberExpression") || releaseMember.computed || !isNodeOfType(releaseMember.property, "Identifier")) return false;
|
|
9575
|
+
const releaseReceiverKey = resolveExpressionKey$1(releaseMember.object, context);
|
|
9576
|
+
if (releaseReceiverKey === null || releaseReceiverKey !== resolveExpressionKey$1(callExpression.arguments?.[0], context)) return false;
|
|
9577
|
+
const releaseVerbName = releaseMember.property.name;
|
|
9578
|
+
if (usage.kind === "socket") return usage.handleKey === releaseReceiverKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
9579
|
+
return usage.kind === "subscribe" && usage.handleKey === releaseReceiverKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName));
|
|
9580
|
+
};
|
|
9581
|
+
const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
9582
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9583
|
+
const doesReturnedValueReleaseUsage = (returnedValue) => {
|
|
9584
|
+
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) return true;
|
|
9585
|
+
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9586
|
+
if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) return true;
|
|
9587
|
+
return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
|
|
9588
|
+
};
|
|
9589
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return doesReturnedValueReleaseUsage(stripParenExpression(callback.body));
|
|
9590
|
+
const matchingCleanupReturns = [];
|
|
9591
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
9592
|
+
if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedValueReleaseUsage(stripParenExpression(child.argument))) matchingCleanupReturns.push(child);
|
|
9593
|
+
});
|
|
9594
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
|
|
9595
|
+
};
|
|
9596
|
+
const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
9597
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
9598
|
+
const functionCfg = context.cfg.cfgFor(callback);
|
|
9599
|
+
const usageBlock = functionCfg?.blockOf(usage.node);
|
|
9600
|
+
const usageStart = getRangeStart(usage.node);
|
|
9601
|
+
if (!functionCfg || !usageBlock || usageStart === null) return false;
|
|
9602
|
+
const findHandleGuard = (releaseCall) => {
|
|
9603
|
+
if (usage.handleKey === null) return null;
|
|
9604
|
+
let ancestor = releaseCall.parent;
|
|
9605
|
+
while (ancestor && ancestor !== callback.body) {
|
|
9606
|
+
if (isNodeOfType(ancestor, "IfStatement")) return ancestor.alternate === null && resolveExpressionKey$1(ancestor.test, context) === usage.handleKey && getRangeStart(ancestor) !== null && (getRangeStart(ancestor) ?? usageStart) < usageStart ? ancestor : null;
|
|
9607
|
+
ancestor = ancestor.parent;
|
|
9608
|
+
}
|
|
9609
|
+
return null;
|
|
9610
|
+
};
|
|
9611
|
+
const matchingReleaseAnchors = [];
|
|
9612
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
9613
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
9614
|
+
const releaseStart = getRangeStart(child);
|
|
9615
|
+
const handleGuard = findHandleGuard(child);
|
|
9616
|
+
if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
|
|
9617
|
+
if (doesReleaseCallMatchUsage(child, usage, context)) {
|
|
9618
|
+
matchingReleaseAnchors.push(handleGuard ?? child);
|
|
9619
|
+
return;
|
|
9620
|
+
}
|
|
9621
|
+
const helperFunction = resolveStableValue(child.callee, context);
|
|
9622
|
+
if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context)) matchingReleaseAnchors.push(handleGuard ?? child);
|
|
9623
|
+
});
|
|
9624
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingReleaseAnchors, context);
|
|
9625
|
+
};
|
|
9626
|
+
const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
9627
|
+
const componentFunction = findEnclosingFunction(callback);
|
|
9628
|
+
if (!componentFunction || !isNodeOfType(componentFunction, "ArrowFunctionExpression") && !isNodeOfType(componentFunction, "FunctionExpression") && !isNodeOfType(componentFunction, "FunctionDeclaration")) return false;
|
|
9629
|
+
let didFindUnmountCleanup = false;
|
|
9630
|
+
walkAst(componentFunction.body, (child) => {
|
|
9631
|
+
if (didFindUnmountCleanup) return false;
|
|
9632
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction(child) !== componentFunction) return;
|
|
9633
|
+
if (!isHookCall$2(child, CLEANUP_EFFECT_HOOK_NAMES)) return;
|
|
9634
|
+
const dependencyList = child.arguments?.[1];
|
|
9635
|
+
if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
|
|
9636
|
+
const cleanupCallback = getEffectCallback(child);
|
|
9637
|
+
if (cleanupCallback && cleanupCallback !== callback && callbackReturnsCleanupForUsage(cleanupCallback, usage, context)) {
|
|
9638
|
+
didFindUnmountCleanup = true;
|
|
9639
|
+
return false;
|
|
9640
|
+
}
|
|
9641
|
+
});
|
|
9642
|
+
return didFindUnmountCleanup;
|
|
9643
|
+
};
|
|
9644
|
+
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
|
|
9273
9645
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
9274
9646
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9275
9647
|
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
@@ -9282,6 +9654,10 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9282
9654
|
if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
|
|
9283
9655
|
const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
|
|
9284
9656
|
if (!returnedValue) return;
|
|
9657
|
+
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
|
|
9658
|
+
matchingCleanupReturns.push(child);
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
9285
9661
|
if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
|
|
9286
9662
|
matchingCleanupReturns.push(child);
|
|
9287
9663
|
return;
|
|
@@ -9297,13 +9673,17 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9297
9673
|
if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
|
|
9298
9674
|
}
|
|
9299
9675
|
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9676
|
+
if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
|
|
9677
|
+
matchingCleanupReturns.push(child);
|
|
9678
|
+
return;
|
|
9679
|
+
}
|
|
9300
9680
|
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
|
|
9301
9681
|
if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
|
|
9302
9682
|
});
|
|
9303
9683
|
return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
|
|
9304
9684
|
};
|
|
9305
9685
|
const findFirstUsageWithoutCleanup = (callback, usages, context) => {
|
|
9306
|
-
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
|
|
9686
|
+
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context) && !hasSplitLifecycleCleanup(callback, usage, context)) return usage;
|
|
9307
9687
|
return null;
|
|
9308
9688
|
};
|
|
9309
9689
|
const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
@@ -11701,7 +12081,7 @@ const forbidDomProps = defineRule({
|
|
|
11701
12081
|
return { JSXOpeningElement(node) {
|
|
11702
12082
|
if (forbidMap.size === 0) return;
|
|
11703
12083
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
11704
|
-
const elementName = node
|
|
12084
|
+
const elementName = resolveJsxElementType(node);
|
|
11705
12085
|
if (isReactComponentName(elementName)) return;
|
|
11706
12086
|
for (const attribute of node.attributes) {
|
|
11707
12087
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11779,7 +12159,7 @@ const forbidElements = defineRule({
|
|
|
11779
12159
|
return {
|
|
11780
12160
|
JSXOpeningElement(node) {
|
|
11781
12161
|
if (forbidMap.size === 0) return;
|
|
11782
|
-
const fullName =
|
|
12162
|
+
const fullName = resolveJsxElementType(node);
|
|
11783
12163
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
11784
12164
|
context.report({
|
|
11785
12165
|
node: node.name,
|
|
@@ -12141,8 +12521,7 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12141
12521
|
const isParagraphElement = (candidate) => {
|
|
12142
12522
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12143
12523
|
const opening = candidate.openingElement;
|
|
12144
|
-
|
|
12145
|
-
return opening.name.name === "p";
|
|
12524
|
+
return resolveJsxElementType(opening) === "p";
|
|
12146
12525
|
};
|
|
12147
12526
|
const findEnclosingParagraph = (openingElement) => {
|
|
12148
12527
|
const owningElement = openingElement.parent;
|
|
@@ -12163,8 +12542,7 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12163
12542
|
severity: "warn",
|
|
12164
12543
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12165
12544
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12166
|
-
|
|
12167
|
-
const childTagName = node.name.name;
|
|
12545
|
+
const childTagName = resolveJsxElementType(node);
|
|
12168
12546
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12169
12547
|
if (!findEnclosingParagraph(node)) return;
|
|
12170
12548
|
context.report({
|
|
@@ -12195,7 +12573,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12195
12573
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12196
12574
|
const opening = jsxElement.openingElement;
|
|
12197
12575
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12198
|
-
const tagName = opening
|
|
12576
|
+
const tagName = resolveJsxElementType(opening);
|
|
12199
12577
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12200
12578
|
return tagName;
|
|
12201
12579
|
};
|
|
@@ -12225,7 +12603,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12225
12603
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12226
12604
|
const opening = ancestor.openingElement;
|
|
12227
12605
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12228
|
-
const ancestorTag = opening
|
|
12606
|
+
const ancestorTag = resolveJsxElementType(opening);
|
|
12229
12607
|
if (ancestorTag.length === 0) {
|
|
12230
12608
|
previous = ancestor;
|
|
12231
12609
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12307,8 +12685,7 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12307
12685
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12308
12686
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12309
12687
|
const opening = candidate.openingElement;
|
|
12310
|
-
|
|
12311
|
-
return opening.name.name === tagName;
|
|
12688
|
+
return resolveJsxElementType(opening) === tagName;
|
|
12312
12689
|
};
|
|
12313
12690
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12314
12691
|
const owningElement = openingElement.parent;
|
|
@@ -12329,8 +12706,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12329
12706
|
severity: "warn",
|
|
12330
12707
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12331
12708
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12332
|
-
|
|
12333
|
-
const tagName = node.name.name;
|
|
12709
|
+
const tagName = resolveJsxElementType(node);
|
|
12334
12710
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12335
12711
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12336
12712
|
context.report({
|
|
@@ -12445,7 +12821,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12445
12821
|
matchByOccurrence: true,
|
|
12446
12822
|
create: skipNonProductionFiles((context) => ({
|
|
12447
12823
|
JSXOpeningElement(node) {
|
|
12448
|
-
if (
|
|
12824
|
+
if (resolveJsxElementType(node) !== "iframe") return;
|
|
12449
12825
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12450
12826
|
if (!sandboxAttr) {
|
|
12451
12827
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -12681,30 +13057,6 @@ const insecureCryptoRisk = defineRule({
|
|
|
12681
13057
|
}
|
|
12682
13058
|
});
|
|
12683
13059
|
//#endregion
|
|
12684
|
-
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
12685
|
-
const findMatchingBracket = (content, openIndex) => {
|
|
12686
|
-
const open = content[openIndex];
|
|
12687
|
-
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
12688
|
-
if (close === "") return -1;
|
|
12689
|
-
let depth = 0;
|
|
12690
|
-
let stringDelimiter = null;
|
|
12691
|
-
for (let index = openIndex; index < content.length; index += 1) {
|
|
12692
|
-
const character = content[index];
|
|
12693
|
-
if (stringDelimiter !== null) {
|
|
12694
|
-
if (character === "\\") index += 1;
|
|
12695
|
-
else if (character === stringDelimiter) stringDelimiter = null;
|
|
12696
|
-
continue;
|
|
12697
|
-
}
|
|
12698
|
-
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
12699
|
-
else if (character === open) depth += 1;
|
|
12700
|
-
else if (character === close) {
|
|
12701
|
-
depth -= 1;
|
|
12702
|
-
if (depth === 0) return index;
|
|
12703
|
-
}
|
|
12704
|
-
}
|
|
12705
|
-
return -1;
|
|
12706
|
-
};
|
|
12707
|
-
//#endregion
|
|
12708
13060
|
//#region src/plugin/rules/security-scan/insecure-session-cookie.ts
|
|
12709
13061
|
const AUTH_COOKIE_NAME_TOKEN = `(?<![A-Za-z0-9])(?:session|sess|sid|connect\\.sid|auth|jwt|access[_-]?token|refresh[_-]?token|id[_-]?token)(?![A-Za-z0-9])`;
|
|
12710
13062
|
const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
|
|
@@ -13461,6 +13813,28 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13461
13813
|
} })
|
|
13462
13814
|
});
|
|
13463
13815
|
//#endregion
|
|
13816
|
+
//#region src/plugin/utils/unwrap-discarded-expression.ts
|
|
13817
|
+
const unwrapDiscardedExpression = (node) => {
|
|
13818
|
+
let expression = isNodeOfType(node, "ExpressionStatement") ? node.expression : node;
|
|
13819
|
+
for (;;) {
|
|
13820
|
+
expression = stripParenExpression(expression);
|
|
13821
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") {
|
|
13822
|
+
expression = expression.argument;
|
|
13823
|
+
continue;
|
|
13824
|
+
}
|
|
13825
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
13826
|
+
const expressions = expression.expressions ?? [];
|
|
13827
|
+
const finalExpression = expressions.at(-1);
|
|
13828
|
+
const prefixExpressions = expressions.slice(0, -1);
|
|
13829
|
+
if (finalExpression && prefixExpressions.length > 0 && prefixExpressions.every((prefixExpression) => isNodeOfType(stripParenExpression(prefixExpression), "Literal"))) {
|
|
13830
|
+
expression = finalExpression;
|
|
13831
|
+
continue;
|
|
13832
|
+
}
|
|
13833
|
+
}
|
|
13834
|
+
return expression;
|
|
13835
|
+
}
|
|
13836
|
+
};
|
|
13837
|
+
//#endregion
|
|
13464
13838
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13465
13839
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13466
13840
|
"forEach",
|
|
@@ -13645,9 +14019,19 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
13645
14019
|
});
|
|
13646
14020
|
return foundAttachment;
|
|
13647
14021
|
};
|
|
14022
|
+
const getStyleAssignment = (node) => {
|
|
14023
|
+
if (!isNodeOfType(node, "ExpressionStatement")) return null;
|
|
14024
|
+
const expression = unwrapDiscardedExpression(node);
|
|
14025
|
+
if (!isNodeOfType(expression, "AssignmentExpression")) return null;
|
|
14026
|
+
if (!isNodeOfType(expression.left, "MemberExpression")) return null;
|
|
14027
|
+
if (!isNodeOfType(expression.left.object, "MemberExpression")) return null;
|
|
14028
|
+
if (!isNodeOfType(expression.left.object.property, "Identifier")) return null;
|
|
14029
|
+
return expression.left.object.property.name === "style" ? expression : null;
|
|
14030
|
+
};
|
|
13648
14031
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
13649
|
-
|
|
13650
|
-
|
|
14032
|
+
const assignment = getStyleAssignment(styleWriteStatement);
|
|
14033
|
+
if (!assignment || !isNodeOfType(assignment.left, "MemberExpression") || !isNodeOfType(assignment.left.object, "MemberExpression")) return false;
|
|
14034
|
+
const elementExpression = assignment.left.object.object;
|
|
13651
14035
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
13652
14036
|
if (!creationRoot) return false;
|
|
13653
14037
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -13659,15 +14043,17 @@ const jsBatchDomCss = defineRule({
|
|
|
13659
14043
|
severity: "warn",
|
|
13660
14044
|
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",
|
|
13661
14045
|
create: (context) => {
|
|
13662
|
-
const
|
|
13663
|
-
|
|
14046
|
+
const writesLayoutAffectingProperty = (node) => {
|
|
14047
|
+
const assignment = getStyleAssignment(node);
|
|
14048
|
+
return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
|
|
14049
|
+
};
|
|
13664
14050
|
return { BlockStatement(node) {
|
|
13665
14051
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
13666
14052
|
if (!perIterationBody) return;
|
|
13667
14053
|
const statements = node.body ?? [];
|
|
13668
14054
|
let layoutReads = null;
|
|
13669
14055
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
13670
|
-
if (
|
|
14056
|
+
if (getStyleAssignment(statements[statementIndex]) === null || getStyleAssignment(statements[statementIndex - 1]) === null) continue;
|
|
13671
14057
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
13672
14058
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
13673
14059
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -14363,6 +14749,18 @@ const jsHoistRegexp = defineRule({
|
|
|
14363
14749
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14364
14750
|
});
|
|
14365
14751
|
//#endregion
|
|
14752
|
+
//#region src/plugin/utils/resolve-first-argument-binding.ts
|
|
14753
|
+
const resolveFirstArgumentBinding = (firstParameter) => {
|
|
14754
|
+
if (!firstParameter) return null;
|
|
14755
|
+
if (!isNodeOfType(firstParameter, "RestElement")) return firstParameter;
|
|
14756
|
+
if (!isNodeOfType(firstParameter.argument, "ArrayPattern")) return null;
|
|
14757
|
+
const elements = firstParameter.argument.elements ?? [];
|
|
14758
|
+
if (elements.length !== 1) return null;
|
|
14759
|
+
const firstBinding = elements[0];
|
|
14760
|
+
if (!firstBinding || isNodeOfType(firstBinding, "RestElement")) return null;
|
|
14761
|
+
return firstBinding;
|
|
14762
|
+
};
|
|
14763
|
+
//#endregion
|
|
14366
14764
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14367
14765
|
const referencesParameter = (expression, parameterName) => {
|
|
14368
14766
|
if (!expression) return false;
|
|
@@ -14373,7 +14771,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14373
14771
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14374
14772
|
const callback = node.arguments?.[0];
|
|
14375
14773
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14376
|
-
const firstParameter = callback.params?.[0];
|
|
14774
|
+
const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
|
|
14377
14775
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14378
14776
|
let predicate = null;
|
|
14379
14777
|
const body = callback.body;
|
|
@@ -15065,9 +15463,21 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15065
15463
|
};
|
|
15066
15464
|
const getResolvedInitializer = (receiver) => {
|
|
15067
15465
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15068
|
-
const
|
|
15069
|
-
|
|
15070
|
-
return
|
|
15466
|
+
const binding = findVariableInitializer(receiver, receiver.name);
|
|
15467
|
+
const initializer = binding?.initializer ?? null;
|
|
15468
|
+
if (!binding || !initializer) return null;
|
|
15469
|
+
const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
|
|
15470
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
15471
|
+
const aliased = findVariableInitializer(initializer, initializer.name);
|
|
15472
|
+
if (aliased?.initializer) return {
|
|
15473
|
+
initializer: aliased.initializer,
|
|
15474
|
+
isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
|
|
15475
|
+
};
|
|
15476
|
+
}
|
|
15477
|
+
return {
|
|
15478
|
+
initializer,
|
|
15479
|
+
isDefault
|
|
15480
|
+
};
|
|
15071
15481
|
};
|
|
15072
15482
|
const isSplitCall = (expression) => {
|
|
15073
15483
|
if (!expression) return false;
|
|
@@ -15233,8 +15643,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15233
15643
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15234
15644
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15235
15645
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15236
|
-
const
|
|
15237
|
-
if (
|
|
15646
|
+
const resolved = getResolvedInitializer(stripped);
|
|
15647
|
+
if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
|
|
15238
15648
|
}
|
|
15239
15649
|
return false;
|
|
15240
15650
|
};
|
|
@@ -15286,8 +15696,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15286
15696
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15287
15697
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15288
15698
|
if (resolvedInitializer) {
|
|
15289
|
-
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15290
|
-
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15699
|
+
if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
|
|
15700
|
+
if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
|
|
15291
15701
|
}
|
|
15292
15702
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15293
15703
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -15700,14 +16110,21 @@ const jsxFilenameExtension = defineRule({
|
|
|
15700
16110
|
});
|
|
15701
16111
|
//#endregion
|
|
15702
16112
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
15703
|
-
const isJsxFragmentElement = (node) => {
|
|
16113
|
+
const isJsxFragmentElement = (node, scopes) => {
|
|
15704
16114
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
15705
16115
|
const elementName = node.name;
|
|
15706
|
-
if (isNodeOfType(elementName, "JSXIdentifier"))
|
|
16116
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
16117
|
+
if (!scopes) return elementName.name === "Fragment";
|
|
16118
|
+
const symbol = resolveConstIdentifierAlias(elementName, scopes);
|
|
16119
|
+
if (!symbol) return elementName.name === "Fragment" && scopes.isGlobalReference(elementName);
|
|
16120
|
+
return isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "Fragment";
|
|
16121
|
+
}
|
|
15707
16122
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
15708
16123
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
15709
|
-
if (elementName.
|
|
15710
|
-
return elementName.
|
|
16124
|
+
if (elementName.property.name !== "Fragment") return false;
|
|
16125
|
+
if (!scopes) return elementName.object.name === "React";
|
|
16126
|
+
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
16127
|
+
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15711
16128
|
}
|
|
15712
16129
|
return false;
|
|
15713
16130
|
};
|
|
@@ -15733,7 +16150,7 @@ const jsxFragments = defineRule({
|
|
|
15733
16150
|
if (mode !== "syntax") return;
|
|
15734
16151
|
if (!node.closingElement) return;
|
|
15735
16152
|
const openingElement = node.openingElement;
|
|
15736
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
16153
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
15737
16154
|
if (openingElement.attributes.length > 0) return;
|
|
15738
16155
|
context.report({
|
|
15739
16156
|
node: openingElement,
|
|
@@ -18414,10 +18831,6 @@ const resolveSettings$29 = (settings) => {
|
|
|
18414
18831
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18415
18832
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18416
18833
|
};
|
|
18417
|
-
const getElementName = (node) => {
|
|
18418
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18419
|
-
return null;
|
|
18420
|
-
};
|
|
18421
18834
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18422
18835
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18423
18836
|
const explicit = options.components?.[elementName];
|
|
@@ -18437,7 +18850,8 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18437
18850
|
create: (context) => {
|
|
18438
18851
|
const options = resolveSettings$29(context.settings);
|
|
18439
18852
|
return { JSXOpeningElement(node) {
|
|
18440
|
-
|
|
18853
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18854
|
+
const elementName = resolveJsxElementType(node);
|
|
18441
18855
|
if (!elementName) return;
|
|
18442
18856
|
for (const attribute of node.attributes) {
|
|
18443
18857
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18624,11 +19038,6 @@ const checkTarget = (attributeValue) => {
|
|
|
18624
19038
|
alternate: false
|
|
18625
19039
|
};
|
|
18626
19040
|
};
|
|
18627
|
-
const getOpeningElementName = (node) => {
|
|
18628
|
-
const name = node.name;
|
|
18629
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18630
|
-
return null;
|
|
18631
|
-
};
|
|
18632
19041
|
const jsxNoTargetBlank = defineRule({
|
|
18633
19042
|
id: "jsx-no-target-blank",
|
|
18634
19043
|
title: "Unsafe target=_blank link",
|
|
@@ -18648,7 +19057,8 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18648
19057
|
return settings.formComponents.has(tagName);
|
|
18649
19058
|
};
|
|
18650
19059
|
return { JSXOpeningElement(node) {
|
|
18651
|
-
|
|
19060
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
19061
|
+
const tagName = resolveJsxElementType(node);
|
|
18652
19062
|
if (!tagName) return;
|
|
18653
19063
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18654
19064
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -18881,7 +19291,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
18881
19291
|
return {
|
|
18882
19292
|
JSXElement(node) {
|
|
18883
19293
|
const openingElement = node.openingElement;
|
|
18884
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
19294
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
18885
19295
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
18886
19296
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
18887
19297
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20123,11 +20533,24 @@ const hasDirective = (programNode, directive) => {
|
|
|
20123
20533
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20124
20534
|
};
|
|
20125
20535
|
//#endregion
|
|
20126
|
-
//#region src/plugin/utils/
|
|
20127
|
-
const
|
|
20128
|
-
|
|
20129
|
-
|
|
20130
|
-
|
|
20536
|
+
//#region src/plugin/utils/unwrap-object-integrity-expression.ts
|
|
20537
|
+
const OBJECT_INTEGRITY_METHOD_NAMES = new Set([
|
|
20538
|
+
"freeze",
|
|
20539
|
+
"seal",
|
|
20540
|
+
"preventExtensions"
|
|
20541
|
+
]);
|
|
20542
|
+
const OBJECT_FREEZE_OR_SEAL_METHOD_NAMES = new Set(["freeze", "seal"]);
|
|
20543
|
+
const unwrapObjectIntegrityExpression = (node, scopes, methodNames = OBJECT_INTEGRITY_METHOD_NAMES) => {
|
|
20544
|
+
let expression = stripParenExpression(node);
|
|
20545
|
+
while (isNodeOfType(expression, "CallExpression")) {
|
|
20546
|
+
const callee = stripParenExpression(expression.callee);
|
|
20547
|
+
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;
|
|
20548
|
+
const wrappedExpression = expression.arguments[0];
|
|
20549
|
+
if (!wrappedExpression || isNodeOfType(wrappedExpression, "SpreadElement")) break;
|
|
20550
|
+
expression = stripParenExpression(wrappedExpression);
|
|
20551
|
+
}
|
|
20552
|
+
return expression;
|
|
20553
|
+
};
|
|
20131
20554
|
//#endregion
|
|
20132
20555
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20133
20556
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20153,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20153
20576
|
},
|
|
20154
20577
|
VariableDeclarator(node) {
|
|
20155
20578
|
if (!fileHasUseClient) return;
|
|
20156
|
-
if (!isComponentAssignment(node)) return;
|
|
20157
|
-
if (!isInlineFunctionExpression(node.init)) return;
|
|
20158
|
-
if (!node.init.async) return;
|
|
20159
20579
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
20580
|
+
if (!isUppercaseName(node.id.name) || !node.init) return;
|
|
20581
|
+
const componentFunction = unwrapObjectIntegrityExpression(node.init, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES);
|
|
20582
|
+
if (!isInlineFunctionExpression(componentFunction) || !componentFunction.async) return;
|
|
20160
20583
|
context.report({
|
|
20161
20584
|
node,
|
|
20162
20585
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -20458,7 +20881,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20458
20881
|
severity: "warn",
|
|
20459
20882
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20460
20883
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20461
|
-
if (
|
|
20884
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
20462
20885
|
const attributes = node.attributes ?? [];
|
|
20463
20886
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20464
20887
|
if (downloadAttribute) {
|
|
@@ -20654,7 +21077,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20654
21077
|
severity: "warn",
|
|
20655
21078
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20656
21079
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20657
|
-
if (
|
|
21080
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20658
21081
|
const attributes = node.attributes ?? [];
|
|
20659
21082
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20660
21083
|
if (!relAttribute?.value) return;
|
|
@@ -20762,7 +21185,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
20762
21185
|
severity: "warn",
|
|
20763
21186
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
20764
21187
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20765
|
-
if (
|
|
21188
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20766
21189
|
const attributes = node.attributes ?? [];
|
|
20767
21190
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
20768
21191
|
if (!hrefAttribute?.value) return;
|
|
@@ -20937,7 +21360,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
20937
21360
|
create: (context) => {
|
|
20938
21361
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
20939
21362
|
return { JSXOpeningElement(node) {
|
|
20940
|
-
if (
|
|
21363
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
20941
21364
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
20942
21365
|
const programRoot = findProgramRoot(node);
|
|
20943
21366
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -20984,8 +21407,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
20984
21407
|
severity: "warn",
|
|
20985
21408
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
20986
21409
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20987
|
-
|
|
20988
|
-
if (
|
|
21410
|
+
const elementName = resolveJsxElementType(node);
|
|
21411
|
+
if (elementName !== "script" && elementName !== "Script") return;
|
|
20989
21412
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
20990
21413
|
if (!srcAttribute?.value) return;
|
|
20991
21414
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -22793,10 +23216,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
22793
23216
|
if (!declaringNode) return false;
|
|
22794
23217
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
22795
23218
|
}));
|
|
23219
|
+
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23220
|
+
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23221
|
+
let declaringFunction = bindingNode.parent;
|
|
23222
|
+
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23223
|
+
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23224
|
+
};
|
|
22796
23225
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
22797
23226
|
if (def.type !== "Parameter") return false;
|
|
22798
|
-
|
|
22799
|
-
return isFunctionLike$1(bindingParent);
|
|
23227
|
+
return isWholePropsParameterBinding(def.name);
|
|
22800
23228
|
}));
|
|
22801
23229
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
22802
23230
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -25793,6 +26221,56 @@ const noBarrelImport = defineRule({
|
|
|
25793
26221
|
}
|
|
25794
26222
|
});
|
|
25795
26223
|
//#endregion
|
|
26224
|
+
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26225
|
+
const collectReturnedExpressions = (functionNode) => {
|
|
26226
|
+
if (!isFunctionLike$1(functionNode)) return [];
|
|
26227
|
+
const body = functionNode.body;
|
|
26228
|
+
if (!body) return [];
|
|
26229
|
+
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26230
|
+
const returnedExpressions = [];
|
|
26231
|
+
walkAst(body, (node) => {
|
|
26232
|
+
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26233
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26234
|
+
});
|
|
26235
|
+
return returnedExpressions;
|
|
26236
|
+
};
|
|
26237
|
+
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26238
|
+
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26239
|
+
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26240
|
+
const functionMatches = (candidateFunction) => {
|
|
26241
|
+
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26242
|
+
visitedFunctions.add(candidateFunction);
|
|
26243
|
+
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26244
|
+
};
|
|
26245
|
+
const expressionMatches = (expression) => {
|
|
26246
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
26247
|
+
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26248
|
+
visitedExpressions.add(unwrappedExpression);
|
|
26249
|
+
if (matchesExpression(unwrappedExpression)) return true;
|
|
26250
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26251
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26252
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26253
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
26254
|
+
if (isFunctionLike$1(initializer)) return false;
|
|
26255
|
+
return expressionMatches(initializer);
|
|
26256
|
+
}
|
|
26257
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26258
|
+
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26259
|
+
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26260
|
+
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26261
|
+
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26262
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26263
|
+
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26264
|
+
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26265
|
+
return functionMatches(candidateFunction);
|
|
26266
|
+
}
|
|
26267
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26268
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26269
|
+
return false;
|
|
26270
|
+
};
|
|
26271
|
+
return functionMatches(functionNode);
|
|
26272
|
+
};
|
|
26273
|
+
//#endregion
|
|
25796
26274
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
25797
26275
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
25798
26276
|
"FunctionDeclaration",
|
|
@@ -25812,12 +26290,13 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
25812
26290
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
25813
26291
|
};
|
|
25814
26292
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26293
|
+
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
25815
26294
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
25816
26295
|
let hasRenderOutput = false;
|
|
25817
26296
|
walkAst(rootNode, (node) => {
|
|
25818
26297
|
if (hasRenderOutput) return false;
|
|
25819
26298
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
25820
|
-
if (
|
|
26299
|
+
if (isRenderOutputExpression(node, scopes)) {
|
|
25821
26300
|
hasRenderOutput = true;
|
|
25822
26301
|
return false;
|
|
25823
26302
|
}
|
|
@@ -25828,7 +26307,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
25828
26307
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
25829
26308
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
25830
26309
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
25831
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26310
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
|
|
25832
26311
|
renderOutputCache.set(functionNode, {
|
|
25833
26312
|
scopes,
|
|
25834
26313
|
hasRenderOutput
|
|
@@ -25836,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
25836
26315
|
return hasRenderOutput;
|
|
25837
26316
|
};
|
|
25838
26317
|
//#endregion
|
|
26318
|
+
//#region src/plugin/utils/is-component-assignment.ts
|
|
26319
|
+
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"));
|
|
26320
|
+
//#endregion
|
|
25839
26321
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
25840
26322
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
25841
26323
|
//#endregion
|
|
@@ -28293,7 +28775,7 @@ const noDisabledZoom = defineRule({
|
|
|
28293
28775
|
category: "Accessibility",
|
|
28294
28776
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28295
28777
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28296
|
-
if (
|
|
28778
|
+
if (resolveJsxElementType(node) !== "meta") return;
|
|
28297
28779
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28298
28780
|
if (!nameAttr?.value) return;
|
|
28299
28781
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -28683,7 +29165,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
28683
29165
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
28684
29166
|
for (const statement of componentBody.body ?? []) {
|
|
28685
29167
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
28686
|
-
const expression = statement
|
|
29168
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
28687
29169
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
28688
29170
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
28689
29171
|
effectCalls.push(expression);
|
|
@@ -31465,7 +31947,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31465
31947
|
severity: "warn",
|
|
31466
31948
|
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.",
|
|
31467
31949
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31468
|
-
if (
|
|
31950
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
31469
31951
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31470
31952
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31471
31953
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31706,7 +32188,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
31706
32188
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
31707
32189
|
return {
|
|
31708
32190
|
JSXOpeningElement(node) {
|
|
31709
|
-
if (
|
|
32191
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
31710
32192
|
let typeAttribute = null;
|
|
31711
32193
|
let typeAttributeIndex = null;
|
|
31712
32194
|
let indeterminateAttribute = null;
|
|
@@ -31885,11 +32367,12 @@ const isMemoCall = (node) => {
|
|
|
31885
32367
|
};
|
|
31886
32368
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
31887
32369
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
31888
|
-
const isInlineReference = (node) => {
|
|
31889
|
-
|
|
31890
|
-
if (isNodeOfType(
|
|
31891
|
-
if (isNodeOfType(
|
|
31892
|
-
if (isNodeOfType(
|
|
32370
|
+
const isInlineReference = (node, scopes) => {
|
|
32371
|
+
const referenceNode = unwrapObjectIntegrityExpression(node, scopes);
|
|
32372
|
+
if (isNodeOfType(referenceNode, "ArrowFunctionExpression") || isNodeOfType(referenceNode, "FunctionExpression") || isNodeOfType(referenceNode, "CallExpression") && isNodeOfType(referenceNode.callee, "MemberExpression") && isNodeOfType(referenceNode.callee.property, "Identifier") && referenceNode.callee.property.name === "bind") return "functions";
|
|
32373
|
+
if (isNodeOfType(referenceNode, "ObjectExpression")) return "objects";
|
|
32374
|
+
if (isNodeOfType(referenceNode, "ArrayExpression")) return "Arrays";
|
|
32375
|
+
if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
|
|
31893
32376
|
return null;
|
|
31894
32377
|
};
|
|
31895
32378
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -31919,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
31919
32402
|
let elementName = null;
|
|
31920
32403
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
31921
32404
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
31922
|
-
const propType = isInlineReference(node.value.expression);
|
|
32405
|
+
const propType = isInlineReference(node.value.expression, context.scopes);
|
|
31923
32406
|
if (propType) context.report({
|
|
31924
32407
|
node: node.value.expression,
|
|
31925
32408
|
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`
|
|
@@ -32778,11 +33261,13 @@ const noManyBooleanProps = defineRule({
|
|
|
32778
33261
|
};
|
|
32779
33262
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
32780
33263
|
if (!param) return;
|
|
33264
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33265
|
+
if (!propsBinding) return;
|
|
32781
33266
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
32782
|
-
if (isNodeOfType(
|
|
33267
|
+
if (isNodeOfType(propsBinding, "ObjectPattern")) {
|
|
32783
33268
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
32784
33269
|
const booleanLikePropNames = [];
|
|
32785
|
-
for (const property of
|
|
33270
|
+
for (const property of propsBinding.properties ?? []) {
|
|
32786
33271
|
if (!isNodeOfType(property, "Property")) continue;
|
|
32787
33272
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
32788
33273
|
if (!keyName) continue;
|
|
@@ -32793,7 +33278,7 @@ const noManyBooleanProps = defineRule({
|
|
|
32793
33278
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
32794
33279
|
return;
|
|
32795
33280
|
}
|
|
32796
|
-
if (isNodeOfType(
|
|
33281
|
+
if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
|
|
32797
33282
|
};
|
|
32798
33283
|
return {
|
|
32799
33284
|
FunctionDeclaration(node) {
|
|
@@ -32915,7 +33400,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
32915
33400
|
if (mirrorBindings.length === 0) return;
|
|
32916
33401
|
for (const statement of componentBody.body ?? []) {
|
|
32917
33402
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
32918
|
-
const effectCall = statement
|
|
33403
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
32919
33404
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
32920
33405
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
32921
33406
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -32929,7 +33414,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
32929
33414
|
const bodyStatements = getCallbackStatements(callback);
|
|
32930
33415
|
if (bodyStatements.length !== 1) continue;
|
|
32931
33416
|
const onlyStatement = bodyStatements[0];
|
|
32932
|
-
const expression =
|
|
33417
|
+
const expression = unwrapDiscardedExpression(onlyStatement);
|
|
32933
33418
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
32934
33419
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
32935
33420
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -34953,6 +35438,14 @@ const isHandlerBagArgument = (analysis, argument) => {
|
|
|
34953
35438
|
};
|
|
34954
35439
|
const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
|
|
34955
35440
|
const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
35441
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
35442
|
+
"useIntersectionObserver",
|
|
35443
|
+
"useMatchMedia",
|
|
35444
|
+
"useMediaQuery",
|
|
35445
|
+
"useResizeObserver",
|
|
35446
|
+
"useVisibility",
|
|
35447
|
+
"useWindowSize"
|
|
35448
|
+
]);
|
|
34956
35449
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
34957
35450
|
const node = def.node;
|
|
34958
35451
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -34975,6 +35468,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
34975
35468
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
34976
35469
|
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isProp(analysis, downstreamRef)));
|
|
34977
35470
|
};
|
|
35471
|
+
const isExternalSubscriptionHookRef = (ref) => {
|
|
35472
|
+
const identifier = ref.identifier;
|
|
35473
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
35474
|
+
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
35475
|
+
return Boolean(ref.resolved?.defs.some((def) => {
|
|
35476
|
+
const node = def.node;
|
|
35477
|
+
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
35478
|
+
const initializer = stripParenExpression(node.init);
|
|
35479
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
35480
|
+
const callee = stripParenExpression(initializer.callee);
|
|
35481
|
+
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
|
|
35482
|
+
}));
|
|
35483
|
+
};
|
|
34978
35484
|
const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
|
|
34979
35485
|
const isCalleePosition = (identifier) => {
|
|
34980
35486
|
const parent = identifier.parent;
|
|
@@ -35036,10 +35542,11 @@ const noPassDataToParent = defineRule({
|
|
|
35036
35542
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
35037
35543
|
}
|
|
35038
35544
|
return getDownstreamRefs(analysis, argument);
|
|
35039
|
-
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
35545
|
+
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
35040
35546
|
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
35041
35547
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
35042
35548
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
35549
|
+
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
35043
35550
|
if (isProp(analysis, argRef)) return false;
|
|
35044
35551
|
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
35045
35552
|
if (isRefCurrent(argRef)) return false;
|
|
@@ -35523,7 +36030,7 @@ const noPreventDefault = defineRule({
|
|
|
35523
36030
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35524
36031
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35525
36032
|
return { JSXOpeningElement(node) {
|
|
35526
|
-
const elementName =
|
|
36033
|
+
const elementName = resolveJsxElementType(node);
|
|
35527
36034
|
if (!elementName) return;
|
|
35528
36035
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35529
36036
|
if (!targetEventProps) return;
|
|
@@ -37008,7 +37515,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37008
37515
|
return expressionReadsStateValue(argument, stateName);
|
|
37009
37516
|
};
|
|
37010
37517
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37011
|
-
const expression =
|
|
37518
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
37012
37519
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37013
37520
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37014
37521
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -37357,7 +37864,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
37357
37864
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
37358
37865
|
for (const statement of functionBody.body ?? []) {
|
|
37359
37866
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37360
|
-
const effectCall = statement
|
|
37867
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
37361
37868
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
37362
37869
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
37363
37870
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -37987,9 +38494,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
37987
38494
|
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.",
|
|
37988
38495
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
37989
38496
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
37990
|
-
const
|
|
38497
|
+
const elementName = resolveJsxElementType(node);
|
|
38498
|
+
const firstCharacter = elementName.charCodeAt(0);
|
|
37991
38499
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
37992
|
-
if (
|
|
38500
|
+
if (elementName.includes("-")) return;
|
|
37993
38501
|
for (const attribute of node.attributes) {
|
|
37994
38502
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
37995
38503
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38377,8 +38885,7 @@ const noUncontrolledInput = defineRule({
|
|
|
38377
38885
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38378
38886
|
walkAst(componentBody, (child) => {
|
|
38379
38887
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38380
|
-
|
|
38381
|
-
const tagName = child.name.name;
|
|
38888
|
+
const tagName = resolveJsxElementType(child);
|
|
38382
38889
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38383
38890
|
const attributes = child.attributes ?? [];
|
|
38384
38891
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38439,7 +38946,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38439
38946
|
severity: "warn",
|
|
38440
38947
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38441
38948
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38442
|
-
if (
|
|
38949
|
+
if (resolveJsxElementType(node) !== "script") return;
|
|
38443
38950
|
const attributes = node.attributes ?? [];
|
|
38444
38951
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38445
38952
|
if (!srcAttribute) return;
|
|
@@ -39654,7 +40161,7 @@ const noUnknownProperty = defineRule({
|
|
|
39654
40161
|
}
|
|
39655
40162
|
if (fileIsNonReactJsx) return;
|
|
39656
40163
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39657
|
-
const elementType = node
|
|
40164
|
+
const elementType = resolveJsxElementType(node);
|
|
39658
40165
|
const firstCharacter = elementType.charCodeAt(0);
|
|
39659
40166
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
39660
40167
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -39832,20 +40339,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
39832
40339
|
});
|
|
39833
40340
|
return found;
|
|
39834
40341
|
};
|
|
40342
|
+
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
39835
40343
|
const isReactClassComponent = (classNode) => {
|
|
39836
40344
|
if (isEs6Component(classNode)) return true;
|
|
39837
40345
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
39838
40346
|
};
|
|
39839
|
-
const findEnclosingComponent = (node) => {
|
|
40347
|
+
const findEnclosingComponent = (node, scopes) => {
|
|
39840
40348
|
let walker = node.parent;
|
|
39841
40349
|
while (walker) {
|
|
39842
40350
|
if (isFunctionLike$1(walker)) {
|
|
39843
40351
|
const componentName = inferFunctionLikeName(walker);
|
|
39844
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40352
|
+
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
|
|
39845
40353
|
component: walker,
|
|
39846
40354
|
name: componentName
|
|
39847
40355
|
};
|
|
39848
|
-
if (!componentName &&
|
|
40356
|
+
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
39849
40357
|
component: walker,
|
|
39850
40358
|
name: null
|
|
39851
40359
|
};
|
|
@@ -40084,7 +40592,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40084
40592
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40085
40593
|
if (settings.allowAsProps) return;
|
|
40086
40594
|
}
|
|
40087
|
-
const enclosing = findEnclosingComponent(candidateNode);
|
|
40595
|
+
const enclosing = findEnclosingComponent(candidateNode, context.scopes);
|
|
40088
40596
|
if (!enclosing) return;
|
|
40089
40597
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40090
40598
|
queuedReports.push({
|
|
@@ -40095,7 +40603,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40095
40603
|
});
|
|
40096
40604
|
};
|
|
40097
40605
|
const checkFunctionLike = (node) => {
|
|
40098
|
-
if (!
|
|
40606
|
+
if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
|
|
40099
40607
|
const inferredName = inferFunctionLikeName(node);
|
|
40100
40608
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40101
40609
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41355,7 +41863,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41355
41863
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41356
41864
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41357
41865
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41358
|
-
const tagName = node
|
|
41866
|
+
const tagName = resolveJsxElementType(node);
|
|
41359
41867
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41360
41868
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41361
41869
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41375,7 +41883,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41375
41883
|
]);
|
|
41376
41884
|
const isTextLikeInput = (openingElement) => {
|
|
41377
41885
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41378
|
-
const tagName = openingElement
|
|
41886
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
41379
41887
|
if (tagName === "textarea") return true;
|
|
41380
41888
|
if (tagName !== "input") return false;
|
|
41381
41889
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41532,8 +42040,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41532
42040
|
]);
|
|
41533
42041
|
const collectBooleanPropBindings = (param) => {
|
|
41534
42042
|
const bindings = /* @__PURE__ */ new Set();
|
|
41535
|
-
|
|
41536
|
-
|
|
42043
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
42044
|
+
if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
|
|
42045
|
+
for (const property of propsBinding.properties ?? []) {
|
|
41537
42046
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41538
42047
|
if (property.computed) continue;
|
|
41539
42048
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -41794,7 +42303,7 @@ const preferHtmlDialog = defineRule({
|
|
|
41794
42303
|
},
|
|
41795
42304
|
JSXOpeningElement(node) {
|
|
41796
42305
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41797
|
-
const tagName = node
|
|
42306
|
+
const tagName = resolveJsxElementType(node);
|
|
41798
42307
|
if (tagName === "dialog") return;
|
|
41799
42308
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41800
42309
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43341,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43341
43850
|
}
|
|
43342
43851
|
return null;
|
|
43343
43852
|
};
|
|
43344
|
-
const resolveHookNameFromInitializer = (initializer) => {
|
|
43853
|
+
const resolveHookNameFromInitializer = (initializer, scopes) => {
|
|
43345
43854
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43346
43855
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43347
|
-
const
|
|
43348
|
-
if (
|
|
43349
|
-
if (!isNodeOfType(
|
|
43350
|
-
return resolveTanstackQueryHookName(
|
|
43856
|
+
const resolvedSymbol = resolveConstIdentifierAlias(initializer, scopes);
|
|
43857
|
+
if (resolvedSymbol?.kind !== "const" || !resolvedSymbol.initializer) return null;
|
|
43858
|
+
if (!isNodeOfType(resolvedSymbol.initializer, "CallExpression")) return null;
|
|
43859
|
+
return resolveTanstackQueryHookName(resolvedSymbol.initializer);
|
|
43351
43860
|
};
|
|
43352
43861
|
const queryNoRestDestructuring = defineRule({
|
|
43353
43862
|
id: "query-no-rest-destructuring",
|
|
@@ -43360,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43360
43869
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43361
43870
|
if (!node.init) return;
|
|
43362
43871
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43363
|
-
const hookName = resolveHookNameFromInitializer(node.init);
|
|
43872
|
+
const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
|
|
43364
43873
|
if (!hookName) return;
|
|
43365
43874
|
context.report({
|
|
43366
43875
|
node: node.id,
|
|
@@ -43831,7 +44340,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
43831
44340
|
severity: "warn",
|
|
43832
44341
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
43833
44342
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
43834
|
-
if (
|
|
44343
|
+
if (resolveJsxElementType(node) !== "svg") return;
|
|
43835
44344
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
43836
44345
|
node,
|
|
43837
44346
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45124,14 +45633,10 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45124
45633
|
//#endregion
|
|
45125
45634
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45126
45635
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45127
|
-
const callbackReturnsJsx = (callback) => {
|
|
45636
|
+
const callbackReturnsJsx = (callback, scopes) => {
|
|
45128
45637
|
if (!callback) return false;
|
|
45129
45638
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45130
|
-
|
|
45131
|
-
if (isJsxExpression(body)) return true;
|
|
45132
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45133
|
-
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45134
|
-
return false;
|
|
45639
|
+
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
|
|
45135
45640
|
};
|
|
45136
45641
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45137
45642
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45209,7 +45714,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45209
45714
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45210
45715
|
for (const declarator of stmt.declarations ?? []) {
|
|
45211
45716
|
const init = declarator.init;
|
|
45212
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45717
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
|
|
45213
45718
|
memoNode = declarator;
|
|
45214
45719
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45215
45720
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45275,8 +45780,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45275
45780
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45276
45781
|
const bindings = /* @__PURE__ */ new Map();
|
|
45277
45782
|
const params = functionNode.params ?? [];
|
|
45278
|
-
for (const param of params)
|
|
45279
|
-
|
|
45783
|
+
for (const [parameterIndex, param] of params.entries()) {
|
|
45784
|
+
const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
|
|
45785
|
+
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45786
|
+
}
|
|
45787
|
+
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45280
45788
|
const body = functionNode.body;
|
|
45281
45789
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45282
45790
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -45782,8 +46290,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
45782
46290
|
if (statements.length !== 1) return;
|
|
45783
46291
|
const onlyStatement = statements[0];
|
|
45784
46292
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
45785
|
-
singleAssignment = onlyStatement
|
|
45786
|
-
} else if (body) singleAssignment = body;
|
|
46293
|
+
singleAssignment = unwrapDiscardedExpression(onlyStatement);
|
|
46294
|
+
} else if (body) singleAssignment = unwrapDiscardedExpression(body);
|
|
45787
46295
|
if (!singleAssignment) return;
|
|
45788
46296
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
45789
46297
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|
|
@@ -52703,7 +53211,8 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
52703
53211
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
52704
53212
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
52705
53213
|
const firstArg = node.arguments?.[0];
|
|
52706
|
-
if (!isNodeOfType(firstArg, "
|
|
53214
|
+
if (!firstArg || isNodeOfType(firstArg, "SpreadElement")) return;
|
|
53215
|
+
if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
|
|
52707
53216
|
context.report({
|
|
52708
53217
|
node,
|
|
52709
53218
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|
|
@@ -53390,10 +53899,6 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53390
53899
|
}
|
|
53391
53900
|
return false;
|
|
53392
53901
|
};
|
|
53393
|
-
const getJsxOpeningElementName = (node) => {
|
|
53394
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53395
|
-
return null;
|
|
53396
|
-
};
|
|
53397
53902
|
const stylePropObject = defineRule({
|
|
53398
53903
|
id: "style-prop-object",
|
|
53399
53904
|
title: "Style prop is not an object",
|
|
@@ -53405,7 +53910,8 @@ const stylePropObject = defineRule({
|
|
|
53405
53910
|
const allowSet = new Set(allow);
|
|
53406
53911
|
return {
|
|
53407
53912
|
JSXOpeningElement(node) {
|
|
53408
|
-
|
|
53913
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
53914
|
+
const elementName = resolveJsxElementType(node);
|
|
53409
53915
|
if (elementName && allowSet.has(elementName)) return;
|
|
53410
53916
|
if (elementName) {
|
|
53411
53917
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54110,7 +54616,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54110
54616
|
create: (context) => {
|
|
54111
54617
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54112
54618
|
return { JSXOpeningElement(node) {
|
|
54113
|
-
if (
|
|
54619
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
54114
54620
|
const attributes = node.attributes ?? [];
|
|
54115
54621
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54116
54622
|
if (!hrefAttribute?.value) return;
|
|
@@ -54731,8 +55237,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
54731
55237
|
create: (context) => ({
|
|
54732
55238
|
JSXElement(node) {
|
|
54733
55239
|
const openingElement = node.openingElement;
|
|
54734
|
-
|
|
54735
|
-
const tagName = openingElement.name.name;
|
|
55240
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
54736
55241
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
54737
55242
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
54738
55243
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|