oxlint-plugin-react-doctor 0.7.6-dev.e141d90 → 0.7.6-dev.e877ca7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +219 -636
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1518,37 +1518,6 @@ 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
|
|
1552
1521
|
//#region src/plugin/utils/get-element-type.ts
|
|
1553
1522
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1554
1523
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1561,8 +1530,14 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1561
1530
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1562
1531
|
return a11ySettings;
|
|
1563
1532
|
};
|
|
1533
|
+
const flattenJsxName$2 = (name) => {
|
|
1534
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1535
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1536
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1537
|
+
return "";
|
|
1538
|
+
};
|
|
1564
1539
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1565
|
-
const baseName =
|
|
1540
|
+
const baseName = flattenJsxName$2(openingElement.name);
|
|
1566
1541
|
if (a11ySettings.polymorphicPropName) {
|
|
1567
1542
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1568
1543
|
if (polymorphicAttribute) {
|
|
@@ -2397,9 +2372,8 @@ const anchorIsValid = defineRule({
|
|
|
2397
2372
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2398
2373
|
return { JSXOpeningElement(node) {
|
|
2399
2374
|
if (isTestlikeFile) return;
|
|
2400
|
-
|
|
2401
|
-
if (
|
|
2402
|
-
if (tag !== "a") return;
|
|
2375
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2376
|
+
if (getElementType(node, context.settings) !== "a") return;
|
|
2403
2377
|
let hrefAttribute;
|
|
2404
2378
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2405
2379
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5754,7 +5728,7 @@ const buttonHasType = defineRule({
|
|
|
5754
5728
|
return {
|
|
5755
5729
|
JSXOpeningElement(node) {
|
|
5756
5730
|
if (isTestlikeFile) return;
|
|
5757
|
-
if (
|
|
5731
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "button") return;
|
|
5758
5732
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5759
5733
|
if (!typeAttr) {
|
|
5760
5734
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5944,7 +5918,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5944
5918
|
};
|
|
5945
5919
|
return {
|
|
5946
5920
|
JSXOpeningElement(node) {
|
|
5947
|
-
if (
|
|
5921
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
5948
5922
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5949
5923
|
},
|
|
5950
5924
|
CallExpression(node) {
|
|
@@ -6021,7 +5995,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
6021
5995
|
//#endregion
|
|
6022
5996
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
6023
5997
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
6024
|
-
if (!isNodeOfType(identifier, "Identifier")
|
|
5998
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6025
5999
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6026
6000
|
let symbol = scopes.symbolFor(identifier);
|
|
6027
6001
|
while (symbol?.kind === "const") {
|
|
@@ -6296,131 +6270,6 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6296
6270
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6297
6271
|
};
|
|
6298
6272
|
//#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
|
|
6424
6273
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6425
6274
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6426
6275
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6442,39 +6291,26 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6442
6291
|
severity: "warn",
|
|
6443
6292
|
category: "Correctness",
|
|
6444
6293
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6445
|
-
create: (context) => {
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
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
|
-
}
|
|
6294
|
+
create: (context) => ({ CallExpression(node) {
|
|
6295
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6296
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6297
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6298
|
+
if (receiver.name !== "localStorage") return;
|
|
6299
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6300
|
+
if (node.callee.property.name !== "setItem") return;
|
|
6301
|
+
const keyArg = node.arguments?.[0];
|
|
6302
|
+
if (!keyArg) return;
|
|
6303
|
+
const keyValue = resolveStringKey(keyArg, context);
|
|
6304
|
+
if (keyValue === null) return;
|
|
6305
|
+
if (isVersionedKey(keyValue)) return;
|
|
6306
|
+
const valueArg = node.arguments?.[1];
|
|
6307
|
+
if (!valueArg) return;
|
|
6308
|
+
if (!isJsonStringifyCall(valueArg)) return;
|
|
6309
|
+
context.report({
|
|
6310
|
+
node: keyArg,
|
|
6311
|
+
message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6312
|
+
});
|
|
6313
|
+
} })
|
|
6478
6314
|
});
|
|
6479
6315
|
//#endregion
|
|
6480
6316
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -7093,30 +6929,6 @@ const corsCookieTrustRisk = defineRule({
|
|
|
7093
6929
|
})
|
|
7094
6930
|
});
|
|
7095
6931
|
//#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
|
|
7120
6932
|
//#region src/plugin/rules/security-scan/dangerous-html-sink.ts
|
|
7121
6933
|
const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
|
|
7122
6934
|
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]*)/;
|
|
@@ -7132,7 +6944,6 @@ const I18N_VALUE_PATTERN = /\b(?:t|i18n|translate|formatMessage|intl)\s*[.(]/;
|
|
|
7132
6944
|
const ESCAPING_SERIALIZER_CALL_PATTERN = /^(?:[\w$.]+\.)?(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*)\s*\(/;
|
|
7133
6945
|
const HIGHLIGHTER_LIBRARY_PATTERN = /\b(?:shiki|prism|hljs|highlightjs|getHighlighter|codeToHtml|codeToHast|refractor|lowlight|starry-night)\b|highlight\.js/i;
|
|
7134
6946
|
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;
|
|
7136
6947
|
const BARE_IDENTIFIER_VALUE_PATTERN = /^[\w$]+\s*(?:[;,})\n]|$)/;
|
|
7137
6948
|
const IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN = /^[\w$]+\s*(?:\|\||\?\?)\s*(?:"[^"\n]*"|'[^'\n]*'|`[^`$]*`)\s*(?:[;,})\n]|$)/;
|
|
7138
6949
|
const MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN = /^[\w$]+(?:\.[\w$]+|\[[^\]]*\])+\s*(?:[;,})\n]|$)/;
|
|
@@ -7292,66 +7103,22 @@ const findContainingBlockEndIndex = (fileContent, targetIndex) => {
|
|
|
7292
7103
|
return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
|
|
7293
7104
|
};
|
|
7294
7105
|
const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
|
|
7295
|
-
const initializerPattern = new RegExp(`(const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
|
|
7106
|
+
const initializerPattern = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
|
|
7296
7107
|
let nearestDeclaration = null;
|
|
7297
7108
|
let nearestDeclarationIndex = -1;
|
|
7298
7109
|
for (const match of fileContent.matchAll(initializerPattern)) {
|
|
7299
7110
|
const declarationIndex = match.index;
|
|
7300
7111
|
if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
|
|
7301
7112
|
nearestDeclarationIndex = declarationIndex;
|
|
7302
|
-
const initializer = match[
|
|
7113
|
+
const initializer = match[1];
|
|
7303
7114
|
if (initializer === void 0) continue;
|
|
7304
7115
|
nearestDeclaration = {
|
|
7305
|
-
declarationIndex,
|
|
7306
7116
|
initializer,
|
|
7307
|
-
initializerStartIndex: declarationIndex + match[0].length - initializer.length
|
|
7308
|
-
isImmutable: match[1] === "const"
|
|
7117
|
+
initializerStartIndex: declarationIndex + match[0].length - initializer.length
|
|
7309
7118
|
};
|
|
7310
7119
|
}
|
|
7311
7120
|
return nearestDeclaration;
|
|
7312
7121
|
};
|
|
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
|
-
};
|
|
7355
7122
|
const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
|
|
7356
7123
|
const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
|
|
7357
7124
|
let closestSource = null;
|
|
@@ -7359,16 +7126,12 @@ const findContainingFunctionParameterSource = (identifier, sinkIndex, fileConten
|
|
|
7359
7126
|
for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
|
|
7360
7127
|
const matchIndex = match.index;
|
|
7361
7128
|
if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
|
|
7362
|
-
|
|
7363
|
-
if (bodyEndIndex < sinkIndex) continue;
|
|
7129
|
+
if (findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{")) < sinkIndex) continue;
|
|
7364
7130
|
const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
|
|
7365
7131
|
if (parameterIndex < 0) continue;
|
|
7366
7132
|
closestStartIndex = matchIndex;
|
|
7367
|
-
const functionName = match[1] ?? "";
|
|
7368
7133
|
closestSource = {
|
|
7369
|
-
|
|
7370
|
-
declarationNameIndex: matchIndex + match[0].indexOf(functionName),
|
|
7371
|
-
functionName,
|
|
7134
|
+
functionName: match[1] ?? "",
|
|
7372
7135
|
parameterIndex
|
|
7373
7136
|
};
|
|
7374
7137
|
}
|
|
@@ -7399,56 +7162,28 @@ const splitTopLevelArguments = (argumentText) => {
|
|
|
7399
7162
|
argumentsList.push(argumentText.slice(startIndex).trim());
|
|
7400
7163
|
return argumentsList;
|
|
7401
7164
|
};
|
|
7402
|
-
const
|
|
7403
|
-
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
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) => {
|
|
7165
|
+
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers) => {
|
|
7413
7166
|
const trimmedExpression = expression.trim();
|
|
7414
|
-
if (
|
|
7415
|
-
|
|
7416
|
-
if (
|
|
7417
|
-
if (
|
|
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];
|
|
7175
|
+
if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
|
|
7418
7176
|
visitedIdentifiers.add(identifier);
|
|
7419
7177
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7178
|
+
if (declaration !== null && isHtmlTainted(declaration.initializer, fileContent, sinkIndex, visitedIdentifiers)) return true;
|
|
7420
7179
|
const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
|
|
7421
|
-
if (
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
if (
|
|
7426
|
-
|
|
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);
|
|
7180
|
+
if (parameterSource === null || parameterSource.functionName.length === 0) return false;
|
|
7181
|
+
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(([^)]*)\\)`, "g");
|
|
7182
|
+
for (const callMatch of fileContent.matchAll(callPattern)) {
|
|
7183
|
+
const argument = splitTopLevelArguments(callMatch[1] ?? "")[parameterSource.parameterIndex];
|
|
7184
|
+
if (argument !== void 0 && isHtmlTainted(argument, fileContent, sinkIndex, new Set(visitedIdentifiers))) return true;
|
|
7185
|
+
}
|
|
7186
|
+
return false;
|
|
7452
7187
|
};
|
|
7453
7188
|
const dangerousHtmlSink = defineRule({
|
|
7454
7189
|
id: "dangerous-html-sink",
|
|
@@ -7487,7 +7222,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
7487
7222
|
const longValueTail = HTML_VALUE_START_PATTERN.exec(lines.slice(lineIndex, lineIndex + 1 + STATIC_TEMPLATE_LOOKAHEAD_LINES).join("\n"))?.[1]?.trimStart();
|
|
7488
7223
|
let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
|
|
7489
7224
|
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;
|
|
7490
|
-
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)
|
|
7225
|
+
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
|
|
7491
7226
|
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
|
|
7492
7227
|
if (declarationInitializer !== null) {
|
|
7493
7228
|
if (isPureStringLiteralConcat(declarationInitializer)) continue;
|
|
@@ -7496,23 +7231,17 @@ const dangerousHtmlSink = defineRule({
|
|
|
7496
7231
|
}
|
|
7497
7232
|
if (templateInterpolations === "") continue;
|
|
7498
7233
|
const judgedExpression = templateInterpolations ?? valueExpression;
|
|
7499
|
-
|
|
7500
|
-
if (
|
|
7501
|
-
if (
|
|
7502
|
-
if (!
|
|
7503
|
-
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
|
|
7234
|
+
if (SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
7235
|
+
if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7236
|
+
if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7237
|
+
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set())) continue;
|
|
7504
7238
|
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
7505
|
-
if (
|
|
7239
|
+
if (/highlighted/i.test(valueExpression)) continue;
|
|
7240
|
+
if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
7506
7241
|
if (valueIdentifier !== void 0) {
|
|
7507
7242
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
7508
|
-
|
|
7509
|
-
|
|
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
|
-
}
|
|
7243
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
7244
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
7516
7245
|
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
7517
7246
|
if (new RegExp(`highlight[\\w$]*\\s*\\.map\\(\\s*(?:async\\s+)?\\(?\\s*${escapedIdentifier}\\b`, "i").test(file.content) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
7518
7247
|
}
|
|
@@ -7597,7 +7326,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7597
7326
|
const getOpeningElementTagName = (openingElement) => {
|
|
7598
7327
|
if (!openingElement) return null;
|
|
7599
7328
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7600
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return
|
|
7329
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement.name.name;
|
|
7601
7330
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7602
7331
|
let cursor = openingElement.name;
|
|
7603
7332
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7849,7 +7578,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7849
7578
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7850
7579
|
return { JSXOpeningElement(node) {
|
|
7851
7580
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7852
|
-
const tagName =
|
|
7581
|
+
const tagName = node.name.name;
|
|
7853
7582
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7854
7583
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7855
7584
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -12081,7 +11810,7 @@ const forbidDomProps = defineRule({
|
|
|
12081
11810
|
return { JSXOpeningElement(node) {
|
|
12082
11811
|
if (forbidMap.size === 0) return;
|
|
12083
11812
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12084
|
-
const elementName =
|
|
11813
|
+
const elementName = node.name.name;
|
|
12085
11814
|
if (isReactComponentName(elementName)) return;
|
|
12086
11815
|
for (const attribute of node.attributes) {
|
|
12087
11816
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -12159,7 +11888,7 @@ const forbidElements = defineRule({
|
|
|
12159
11888
|
return {
|
|
12160
11889
|
JSXOpeningElement(node) {
|
|
12161
11890
|
if (forbidMap.size === 0) return;
|
|
12162
|
-
const fullName =
|
|
11891
|
+
const fullName = flattenJsxName$1(node.name);
|
|
12163
11892
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
12164
11893
|
context.report({
|
|
12165
11894
|
node: node.name,
|
|
@@ -12521,7 +12250,8 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12521
12250
|
const isParagraphElement = (candidate) => {
|
|
12522
12251
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12523
12252
|
const opening = candidate.openingElement;
|
|
12524
|
-
|
|
12253
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12254
|
+
return opening.name.name === "p";
|
|
12525
12255
|
};
|
|
12526
12256
|
const findEnclosingParagraph = (openingElement) => {
|
|
12527
12257
|
const owningElement = openingElement.parent;
|
|
@@ -12542,7 +12272,8 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12542
12272
|
severity: "warn",
|
|
12543
12273
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12544
12274
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12545
|
-
|
|
12275
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12276
|
+
const childTagName = node.name.name;
|
|
12546
12277
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12547
12278
|
if (!findEnclosingParagraph(node)) return;
|
|
12548
12279
|
context.report({
|
|
@@ -12573,7 +12304,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12573
12304
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12574
12305
|
const opening = jsxElement.openingElement;
|
|
12575
12306
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12576
|
-
const tagName =
|
|
12307
|
+
const tagName = opening.name.name;
|
|
12577
12308
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12578
12309
|
return tagName;
|
|
12579
12310
|
};
|
|
@@ -12603,7 +12334,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12603
12334
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12604
12335
|
const opening = ancestor.openingElement;
|
|
12605
12336
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12606
|
-
const ancestorTag =
|
|
12337
|
+
const ancestorTag = opening.name.name;
|
|
12607
12338
|
if (ancestorTag.length === 0) {
|
|
12608
12339
|
previous = ancestor;
|
|
12609
12340
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12685,7 +12416,8 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12685
12416
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12686
12417
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12687
12418
|
const opening = candidate.openingElement;
|
|
12688
|
-
|
|
12419
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12420
|
+
return opening.name.name === tagName;
|
|
12689
12421
|
};
|
|
12690
12422
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12691
12423
|
const owningElement = openingElement.parent;
|
|
@@ -12706,7 +12438,8 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12706
12438
|
severity: "warn",
|
|
12707
12439
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12708
12440
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12709
|
-
|
|
12441
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12442
|
+
const tagName = node.name.name;
|
|
12710
12443
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12711
12444
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12712
12445
|
context.report({
|
|
@@ -12821,7 +12554,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12821
12554
|
matchByOccurrence: true,
|
|
12822
12555
|
create: skipNonProductionFiles((context) => ({
|
|
12823
12556
|
JSXOpeningElement(node) {
|
|
12824
|
-
if (
|
|
12557
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
|
|
12825
12558
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12826
12559
|
if (!sandboxAttr) {
|
|
12827
12560
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -13057,6 +12790,30 @@ const insecureCryptoRisk = defineRule({
|
|
|
13057
12790
|
}
|
|
13058
12791
|
});
|
|
13059
12792
|
//#endregion
|
|
12793
|
+
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
12794
|
+
const findMatchingBracket = (content, openIndex) => {
|
|
12795
|
+
const open = content[openIndex];
|
|
12796
|
+
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
12797
|
+
if (close === "") return -1;
|
|
12798
|
+
let depth = 0;
|
|
12799
|
+
let stringDelimiter = null;
|
|
12800
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
12801
|
+
const character = content[index];
|
|
12802
|
+
if (stringDelimiter !== null) {
|
|
12803
|
+
if (character === "\\") index += 1;
|
|
12804
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
12805
|
+
continue;
|
|
12806
|
+
}
|
|
12807
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
12808
|
+
else if (character === open) depth += 1;
|
|
12809
|
+
else if (character === close) {
|
|
12810
|
+
depth -= 1;
|
|
12811
|
+
if (depth === 0) return index;
|
|
12812
|
+
}
|
|
12813
|
+
}
|
|
12814
|
+
return -1;
|
|
12815
|
+
};
|
|
12816
|
+
//#endregion
|
|
13060
12817
|
//#region src/plugin/rules/security-scan/insecure-session-cookie.ts
|
|
13061
12818
|
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])`;
|
|
13062
12819
|
const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
|
|
@@ -13813,28 +13570,6 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13813
13570
|
} })
|
|
13814
13571
|
});
|
|
13815
13572
|
//#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
|
|
13838
13573
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13839
13574
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13840
13575
|
"forEach",
|
|
@@ -14019,19 +13754,9 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
14019
13754
|
});
|
|
14020
13755
|
return foundAttachment;
|
|
14021
13756
|
};
|
|
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
|
-
};
|
|
14031
13757
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
const elementExpression = assignment.left.object.object;
|
|
13758
|
+
if (!isNodeOfType(styleWriteStatement, "ExpressionStatement") || !isNodeOfType(styleWriteStatement.expression, "AssignmentExpression") || !isNodeOfType(styleWriteStatement.expression.left, "MemberExpression") || !isNodeOfType(styleWriteStatement.expression.left.object, "MemberExpression")) return false;
|
|
13759
|
+
const elementExpression = styleWriteStatement.expression.left.object.object;
|
|
14035
13760
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
14036
13761
|
if (!creationRoot) return false;
|
|
14037
13762
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -14043,17 +13768,15 @@ const jsBatchDomCss = defineRule({
|
|
|
14043
13768
|
severity: "warn",
|
|
14044
13769
|
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",
|
|
14045
13770
|
create: (context) => {
|
|
14046
|
-
const
|
|
14047
|
-
|
|
14048
|
-
return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
|
|
14049
|
-
};
|
|
13771
|
+
const isStyleAssignment = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.object, "MemberExpression") && isNodeOfType(node.expression.left.object.property, "Identifier") && node.expression.left.object.property.name === "style";
|
|
13772
|
+
const writesLayoutAffectingProperty = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(node.expression.left.property.name);
|
|
14050
13773
|
return { BlockStatement(node) {
|
|
14051
13774
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
14052
13775
|
if (!perIterationBody) return;
|
|
14053
13776
|
const statements = node.body ?? [];
|
|
14054
13777
|
let layoutReads = null;
|
|
14055
13778
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
14056
|
-
if (
|
|
13779
|
+
if (!isStyleAssignment(statements[statementIndex]) || !isStyleAssignment(statements[statementIndex - 1])) continue;
|
|
14057
13780
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
14058
13781
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
14059
13782
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -14749,18 +14472,6 @@ const jsHoistRegexp = defineRule({
|
|
|
14749
14472
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14750
14473
|
});
|
|
14751
14474
|
//#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
|
|
14764
14475
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14765
14476
|
const referencesParameter = (expression, parameterName) => {
|
|
14766
14477
|
if (!expression) return false;
|
|
@@ -14771,7 +14482,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14771
14482
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14772
14483
|
const callback = node.arguments?.[0];
|
|
14773
14484
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14774
|
-
const firstParameter =
|
|
14485
|
+
const firstParameter = callback.params?.[0];
|
|
14775
14486
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14776
14487
|
let predicate = null;
|
|
14777
14488
|
const body = callback.body;
|
|
@@ -15463,21 +15174,9 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15463
15174
|
};
|
|
15464
15175
|
const getResolvedInitializer = (receiver) => {
|
|
15465
15176
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15466
|
-
const
|
|
15467
|
-
|
|
15468
|
-
|
|
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
|
-
};
|
|
15177
|
+
const initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
|
|
15178
|
+
if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
|
|
15179
|
+
return initializer;
|
|
15481
15180
|
};
|
|
15482
15181
|
const isSplitCall = (expression) => {
|
|
15483
15182
|
if (!expression) return false;
|
|
@@ -15643,8 +15342,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15643
15342
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15644
15343
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15645
15344
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15646
|
-
const
|
|
15647
|
-
if (
|
|
15345
|
+
const initializer = getResolvedInitializer(stripped);
|
|
15346
|
+
if (initializer && isSmallInlineLiteralArray(initializer)) return true;
|
|
15648
15347
|
}
|
|
15649
15348
|
return false;
|
|
15650
15349
|
};
|
|
@@ -15696,8 +15395,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15696
15395
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15697
15396
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15698
15397
|
if (resolvedInitializer) {
|
|
15699
|
-
if (isLikelyStringReceiver(resolvedInitializer
|
|
15700
|
-
if (
|
|
15398
|
+
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15399
|
+
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15701
15400
|
}
|
|
15702
15401
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15703
15402
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -16110,21 +15809,14 @@ const jsxFilenameExtension = defineRule({
|
|
|
16110
15809
|
});
|
|
16111
15810
|
//#endregion
|
|
16112
15811
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
16113
|
-
const isJsxFragmentElement = (node
|
|
15812
|
+
const isJsxFragmentElement = (node) => {
|
|
16114
15813
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
16115
15814
|
const elementName = node.name;
|
|
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
|
-
}
|
|
15815
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) return elementName.name === "Fragment";
|
|
16122
15816
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
16123
15817
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
16124
|
-
if (elementName.
|
|
16125
|
-
|
|
16126
|
-
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
16127
|
-
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15818
|
+
if (elementName.object.name !== "React") return false;
|
|
15819
|
+
return elementName.property.name === "Fragment";
|
|
16128
15820
|
}
|
|
16129
15821
|
return false;
|
|
16130
15822
|
};
|
|
@@ -16150,7 +15842,7 @@ const jsxFragments = defineRule({
|
|
|
16150
15842
|
if (mode !== "syntax") return;
|
|
16151
15843
|
if (!node.closingElement) return;
|
|
16152
15844
|
const openingElement = node.openingElement;
|
|
16153
|
-
if (!isJsxFragmentElement(openingElement
|
|
15845
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
16154
15846
|
if (openingElement.attributes.length > 0) return;
|
|
16155
15847
|
context.report({
|
|
16156
15848
|
node: openingElement,
|
|
@@ -18831,6 +18523,10 @@ const resolveSettings$29 = (settings) => {
|
|
|
18831
18523
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18832
18524
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18833
18525
|
};
|
|
18526
|
+
const getElementName = (node) => {
|
|
18527
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18528
|
+
return null;
|
|
18529
|
+
};
|
|
18834
18530
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18835
18531
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18836
18532
|
const explicit = options.components?.[elementName];
|
|
@@ -18850,8 +18546,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18850
18546
|
create: (context) => {
|
|
18851
18547
|
const options = resolveSettings$29(context.settings);
|
|
18852
18548
|
return { JSXOpeningElement(node) {
|
|
18853
|
-
|
|
18854
|
-
const elementName = resolveJsxElementType(node);
|
|
18549
|
+
const elementName = getElementName(node);
|
|
18855
18550
|
if (!elementName) return;
|
|
18856
18551
|
for (const attribute of node.attributes) {
|
|
18857
18552
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -19038,6 +18733,11 @@ const checkTarget = (attributeValue) => {
|
|
|
19038
18733
|
alternate: false
|
|
19039
18734
|
};
|
|
19040
18735
|
};
|
|
18736
|
+
const getOpeningElementName = (node) => {
|
|
18737
|
+
const name = node.name;
|
|
18738
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18739
|
+
return null;
|
|
18740
|
+
};
|
|
19041
18741
|
const jsxNoTargetBlank = defineRule({
|
|
19042
18742
|
id: "jsx-no-target-blank",
|
|
19043
18743
|
title: "Unsafe target=_blank link",
|
|
@@ -19057,8 +18757,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
19057
18757
|
return settings.formComponents.has(tagName);
|
|
19058
18758
|
};
|
|
19059
18759
|
return { JSXOpeningElement(node) {
|
|
19060
|
-
|
|
19061
|
-
const tagName = resolveJsxElementType(node);
|
|
18760
|
+
const tagName = getOpeningElementName(node);
|
|
19062
18761
|
if (!tagName) return;
|
|
19063
18762
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
19064
18763
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19291,7 +18990,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19291
18990
|
return {
|
|
19292
18991
|
JSXElement(node) {
|
|
19293
18992
|
const openingElement = node.openingElement;
|
|
19294
|
-
if (!isJsxFragmentElement(openingElement
|
|
18993
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
19295
18994
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19296
18995
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19297
18996
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20533,24 +20232,11 @@ const hasDirective = (programNode, directive) => {
|
|
|
20533
20232
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20534
20233
|
};
|
|
20535
20234
|
//#endregion
|
|
20536
|
-
//#region src/plugin/utils/
|
|
20537
|
-
const
|
|
20538
|
-
|
|
20539
|
-
|
|
20540
|
-
|
|
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
|
-
};
|
|
20235
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20236
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20237
|
+
//#endregion
|
|
20238
|
+
//#region src/plugin/utils/is-component-assignment.ts
|
|
20239
|
+
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"));
|
|
20554
20240
|
//#endregion
|
|
20555
20241
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20556
20242
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20576,10 +20262,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20576
20262
|
},
|
|
20577
20263
|
VariableDeclarator(node) {
|
|
20578
20264
|
if (!fileHasUseClient) return;
|
|
20265
|
+
if (!isComponentAssignment(node)) return;
|
|
20266
|
+
if (!isInlineFunctionExpression(node.init)) return;
|
|
20267
|
+
if (!node.init.async) return;
|
|
20579
20268
|
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;
|
|
20583
20269
|
context.report({
|
|
20584
20270
|
node,
|
|
20585
20271
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -20881,7 +20567,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20881
20567
|
severity: "warn",
|
|
20882
20568
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20883
20569
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20884
|
-
if (
|
|
20570
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
20885
20571
|
const attributes = node.attributes ?? [];
|
|
20886
20572
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20887
20573
|
if (downloadAttribute) {
|
|
@@ -21077,7 +20763,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
21077
20763
|
severity: "warn",
|
|
21078
20764
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
21079
20765
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21080
|
-
if (
|
|
20766
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21081
20767
|
const attributes = node.attributes ?? [];
|
|
21082
20768
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
21083
20769
|
if (!relAttribute?.value) return;
|
|
@@ -21185,7 +20871,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21185
20871
|
severity: "warn",
|
|
21186
20872
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21187
20873
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21188
|
-
if (
|
|
20874
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21189
20875
|
const attributes = node.attributes ?? [];
|
|
21190
20876
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21191
20877
|
if (!hrefAttribute?.value) return;
|
|
@@ -21360,7 +21046,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21360
21046
|
create: (context) => {
|
|
21361
21047
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21362
21048
|
return { JSXOpeningElement(node) {
|
|
21363
|
-
if (
|
|
21049
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
21364
21050
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21365
21051
|
const programRoot = findProgramRoot(node);
|
|
21366
21052
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21407,8 +21093,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21407
21093
|
severity: "warn",
|
|
21408
21094
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21409
21095
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21410
|
-
|
|
21411
|
-
if (
|
|
21096
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
21097
|
+
if (node.name.name !== "script" && node.name.name !== "Script") return;
|
|
21412
21098
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21413
21099
|
if (!srcAttribute?.value) return;
|
|
21414
21100
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23216,15 +22902,10 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23216
22902
|
if (!declaringNode) return false;
|
|
23217
22903
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23218
22904
|
}));
|
|
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
|
-
};
|
|
23225
22905
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23226
22906
|
if (def.type !== "Parameter") return false;
|
|
23227
|
-
|
|
22907
|
+
const bindingParent = def.name.parent;
|
|
22908
|
+
return isFunctionLike$1(bindingParent);
|
|
23228
22909
|
}));
|
|
23229
22910
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23230
22911
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26221,56 +25902,6 @@ const noBarrelImport = defineRule({
|
|
|
26221
25902
|
}
|
|
26222
25903
|
});
|
|
26223
25904
|
//#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
|
|
26274
25905
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26275
25906
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26276
25907
|
"FunctionDeclaration",
|
|
@@ -26290,13 +25921,12 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26290
25921
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26291
25922
|
};
|
|
26292
25923
|
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);
|
|
26294
25924
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26295
25925
|
let hasRenderOutput = false;
|
|
26296
25926
|
walkAst(rootNode, (node) => {
|
|
26297
25927
|
if (hasRenderOutput) return false;
|
|
26298
25928
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26299
|
-
if (
|
|
25929
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
|
|
26300
25930
|
hasRenderOutput = true;
|
|
26301
25931
|
return false;
|
|
26302
25932
|
}
|
|
@@ -26307,7 +25937,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26307
25937
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26308
25938
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26309
25939
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26310
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes)
|
|
25940
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26311
25941
|
renderOutputCache.set(functionNode, {
|
|
26312
25942
|
scopes,
|
|
26313
25943
|
hasRenderOutput
|
|
@@ -26315,9 +25945,6 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
26315
25945
|
return hasRenderOutput;
|
|
26316
25946
|
};
|
|
26317
25947
|
//#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
|
|
26321
25948
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
26322
25949
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
26323
25950
|
//#endregion
|
|
@@ -28775,7 +28402,7 @@ const noDisabledZoom = defineRule({
|
|
|
28775
28402
|
category: "Accessibility",
|
|
28776
28403
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28777
28404
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28778
|
-
if (
|
|
28405
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
|
|
28779
28406
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28780
28407
|
if (!nameAttr?.value) return;
|
|
28781
28408
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -29165,7 +28792,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
29165
28792
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
29166
28793
|
for (const statement of componentBody.body ?? []) {
|
|
29167
28794
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
29168
|
-
const expression =
|
|
28795
|
+
const expression = statement.expression;
|
|
29169
28796
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
29170
28797
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
29171
28798
|
effectCalls.push(expression);
|
|
@@ -31947,7 +31574,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31947
31574
|
severity: "warn",
|
|
31948
31575
|
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.",
|
|
31949
31576
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31950
|
-
if (
|
|
31577
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
31951
31578
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31952
31579
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31953
31580
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -32188,7 +31815,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
32188
31815
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
32189
31816
|
return {
|
|
32190
31817
|
JSXOpeningElement(node) {
|
|
32191
|
-
if (
|
|
31818
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
32192
31819
|
let typeAttribute = null;
|
|
32193
31820
|
let typeAttributeIndex = null;
|
|
32194
31821
|
let indeterminateAttribute = null;
|
|
@@ -32367,12 +31994,11 @@ const isMemoCall = (node) => {
|
|
|
32367
31994
|
};
|
|
32368
31995
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
32369
31996
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
32370
|
-
const isInlineReference = (node
|
|
32371
|
-
|
|
32372
|
-
if (isNodeOfType(
|
|
32373
|
-
if (isNodeOfType(
|
|
32374
|
-
if (isNodeOfType(
|
|
32375
|
-
if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
|
|
31997
|
+
const isInlineReference = (node) => {
|
|
31998
|
+
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "bind") return "functions";
|
|
31999
|
+
if (isNodeOfType(node, "ObjectExpression")) return "objects";
|
|
32000
|
+
if (isNodeOfType(node, "ArrayExpression")) return "Arrays";
|
|
32001
|
+
if (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")) return "JSX";
|
|
32376
32002
|
return null;
|
|
32377
32003
|
};
|
|
32378
32004
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -32402,7 +32028,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
32402
32028
|
let elementName = null;
|
|
32403
32029
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
32404
32030
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
32405
|
-
const propType = isInlineReference(node.value.expression
|
|
32031
|
+
const propType = isInlineReference(node.value.expression);
|
|
32406
32032
|
if (propType) context.report({
|
|
32407
32033
|
node: node.value.expression,
|
|
32408
32034
|
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`
|
|
@@ -33261,13 +32887,11 @@ const noManyBooleanProps = defineRule({
|
|
|
33261
32887
|
};
|
|
33262
32888
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33263
32889
|
if (!param) return;
|
|
33264
|
-
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33265
|
-
if (!propsBinding) return;
|
|
33266
32890
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33267
|
-
if (isNodeOfType(
|
|
32891
|
+
if (isNodeOfType(param, "ObjectPattern")) {
|
|
33268
32892
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33269
32893
|
const booleanLikePropNames = [];
|
|
33270
|
-
for (const property of
|
|
32894
|
+
for (const property of param.properties ?? []) {
|
|
33271
32895
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33272
32896
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33273
32897
|
if (!keyName) continue;
|
|
@@ -33278,7 +32902,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33278
32902
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33279
32903
|
return;
|
|
33280
32904
|
}
|
|
33281
|
-
if (isNodeOfType(
|
|
32905
|
+
if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
|
|
33282
32906
|
};
|
|
33283
32907
|
return {
|
|
33284
32908
|
FunctionDeclaration(node) {
|
|
@@ -33400,7 +33024,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33400
33024
|
if (mirrorBindings.length === 0) return;
|
|
33401
33025
|
for (const statement of componentBody.body ?? []) {
|
|
33402
33026
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
33403
|
-
const effectCall =
|
|
33027
|
+
const effectCall = statement.expression;
|
|
33404
33028
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
33405
33029
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
33406
33030
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -33414,7 +33038,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33414
33038
|
const bodyStatements = getCallbackStatements(callback);
|
|
33415
33039
|
if (bodyStatements.length !== 1) continue;
|
|
33416
33040
|
const onlyStatement = bodyStatements[0];
|
|
33417
|
-
const expression =
|
|
33041
|
+
const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
|
|
33418
33042
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
33419
33043
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
33420
33044
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -36030,7 +35654,7 @@ const noPreventDefault = defineRule({
|
|
|
36030
35654
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
36031
35655
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
36032
35656
|
return { JSXOpeningElement(node) {
|
|
36033
|
-
const elementName =
|
|
35657
|
+
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
36034
35658
|
if (!elementName) return;
|
|
36035
35659
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
36036
35660
|
if (!targetEventProps) return;
|
|
@@ -37515,7 +37139,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37515
37139
|
return expressionReadsStateValue(argument, stateName);
|
|
37516
37140
|
};
|
|
37517
37141
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37518
|
-
const expression =
|
|
37142
|
+
const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
|
|
37519
37143
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37520
37144
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37521
37145
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -37864,7 +37488,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
37864
37488
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
37865
37489
|
for (const statement of functionBody.body ?? []) {
|
|
37866
37490
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37867
|
-
const effectCall =
|
|
37491
|
+
const effectCall = statement.expression;
|
|
37868
37492
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
37869
37493
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
37870
37494
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -38494,10 +38118,9 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38494
38118
|
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.",
|
|
38495
38119
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38496
38120
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38497
|
-
const
|
|
38498
|
-
const firstCharacter = elementName.charCodeAt(0);
|
|
38121
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
38499
38122
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38500
|
-
if (
|
|
38123
|
+
if (node.name.name.includes("-")) return;
|
|
38501
38124
|
for (const attribute of node.attributes) {
|
|
38502
38125
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38503
38126
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38885,7 +38508,8 @@ const noUncontrolledInput = defineRule({
|
|
|
38885
38508
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38886
38509
|
walkAst(componentBody, (child) => {
|
|
38887
38510
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38888
|
-
|
|
38511
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
38512
|
+
const tagName = child.name.name;
|
|
38889
38513
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38890
38514
|
const attributes = child.attributes ?? [];
|
|
38891
38515
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38946,7 +38570,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38946
38570
|
severity: "warn",
|
|
38947
38571
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38948
38572
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38949
|
-
if (
|
|
38573
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
|
|
38950
38574
|
const attributes = node.attributes ?? [];
|
|
38951
38575
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38952
38576
|
if (!srcAttribute) return;
|
|
@@ -40161,7 +39785,7 @@ const noUnknownProperty = defineRule({
|
|
|
40161
39785
|
}
|
|
40162
39786
|
if (fileIsNonReactJsx) return;
|
|
40163
39787
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
40164
|
-
const elementType =
|
|
39788
|
+
const elementType = node.name.name;
|
|
40165
39789
|
const firstCharacter = elementType.charCodeAt(0);
|
|
40166
39790
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
40167
39791
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40339,21 +39963,20 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40339
39963
|
});
|
|
40340
39964
|
return found;
|
|
40341
39965
|
};
|
|
40342
|
-
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40343
39966
|
const isReactClassComponent = (classNode) => {
|
|
40344
39967
|
if (isEs6Component(classNode)) return true;
|
|
40345
39968
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40346
39969
|
};
|
|
40347
|
-
const findEnclosingComponent = (node
|
|
39970
|
+
const findEnclosingComponent = (node) => {
|
|
40348
39971
|
let walker = node.parent;
|
|
40349
39972
|
while (walker) {
|
|
40350
39973
|
if (isFunctionLike$1(walker)) {
|
|
40351
39974
|
const componentName = inferFunctionLikeName(walker);
|
|
40352
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
39975
|
+
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
40353
39976
|
component: walker,
|
|
40354
39977
|
name: componentName
|
|
40355
39978
|
};
|
|
40356
|
-
if (!componentName &&
|
|
39979
|
+
if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40357
39980
|
component: walker,
|
|
40358
39981
|
name: null
|
|
40359
39982
|
};
|
|
@@ -40592,7 +40215,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40592
40215
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40593
40216
|
if (settings.allowAsProps) return;
|
|
40594
40217
|
}
|
|
40595
|
-
const enclosing = findEnclosingComponent(candidateNode
|
|
40218
|
+
const enclosing = findEnclosingComponent(candidateNode);
|
|
40596
40219
|
if (!enclosing) return;
|
|
40597
40220
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40598
40221
|
queuedReports.push({
|
|
@@ -40603,7 +40226,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40603
40226
|
});
|
|
40604
40227
|
};
|
|
40605
40228
|
const checkFunctionLike = (node) => {
|
|
40606
|
-
if (!
|
|
40229
|
+
if (!expressionContainsJsxOrCreateElement(node)) return;
|
|
40607
40230
|
const inferredName = inferFunctionLikeName(node);
|
|
40608
40231
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40609
40232
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41863,7 +41486,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41863
41486
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41864
41487
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41865
41488
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41866
|
-
const tagName =
|
|
41489
|
+
const tagName = node.name.name;
|
|
41867
41490
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41868
41491
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41869
41492
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41883,7 +41506,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41883
41506
|
]);
|
|
41884
41507
|
const isTextLikeInput = (openingElement) => {
|
|
41885
41508
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41886
|
-
const tagName =
|
|
41509
|
+
const tagName = openingElement.name.name;
|
|
41887
41510
|
if (tagName === "textarea") return true;
|
|
41888
41511
|
if (tagName !== "input") return false;
|
|
41889
41512
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -42040,9 +41663,8 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
42040
41663
|
]);
|
|
42041
41664
|
const collectBooleanPropBindings = (param) => {
|
|
42042
41665
|
const bindings = /* @__PURE__ */ new Set();
|
|
42043
|
-
|
|
42044
|
-
|
|
42045
|
-
for (const property of propsBinding.properties ?? []) {
|
|
41666
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
41667
|
+
for (const property of param.properties ?? []) {
|
|
42046
41668
|
if (!isNodeOfType(property, "Property")) continue;
|
|
42047
41669
|
if (property.computed) continue;
|
|
42048
41670
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42303,7 +41925,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42303
41925
|
},
|
|
42304
41926
|
JSXOpeningElement(node) {
|
|
42305
41927
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42306
|
-
const tagName =
|
|
41928
|
+
const tagName = node.name.name;
|
|
42307
41929
|
if (tagName === "dialog") return;
|
|
42308
41930
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42309
41931
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43850,13 +43472,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43850
43472
|
}
|
|
43851
43473
|
return null;
|
|
43852
43474
|
};
|
|
43853
|
-
const resolveHookNameFromInitializer = (initializer
|
|
43475
|
+
const resolveHookNameFromInitializer = (initializer) => {
|
|
43854
43476
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43855
43477
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43856
|
-
const
|
|
43857
|
-
if (
|
|
43858
|
-
if (!isNodeOfType(
|
|
43859
|
-
return resolveTanstackQueryHookName(
|
|
43478
|
+
const binding = findVariableInitializer(initializer, initializer.name);
|
|
43479
|
+
if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
|
|
43480
|
+
if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
43481
|
+
return resolveTanstackQueryHookName(binding.initializer);
|
|
43860
43482
|
};
|
|
43861
43483
|
const queryNoRestDestructuring = defineRule({
|
|
43862
43484
|
id: "query-no-rest-destructuring",
|
|
@@ -43869,7 +43491,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43869
43491
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43870
43492
|
if (!node.init) return;
|
|
43871
43493
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43872
|
-
const hookName = resolveHookNameFromInitializer(node.init
|
|
43494
|
+
const hookName = resolveHookNameFromInitializer(node.init);
|
|
43873
43495
|
if (!hookName) return;
|
|
43874
43496
|
context.report({
|
|
43875
43497
|
node: node.id,
|
|
@@ -44340,7 +43962,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44340
43962
|
severity: "warn",
|
|
44341
43963
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44342
43964
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44343
|
-
if (
|
|
43965
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
44344
43966
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44345
43967
|
node,
|
|
44346
43968
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45224,7 +44846,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
|
|
|
45224
44846
|
"useParams",
|
|
45225
44847
|
"usePathname"
|
|
45226
44848
|
]);
|
|
45227
|
-
const findHookCallBindings = (componentBody
|
|
44849
|
+
const findHookCallBindings = (componentBody) => {
|
|
45228
44850
|
const bindings = [];
|
|
45229
44851
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
45230
44852
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -45235,10 +44857,8 @@ const findHookCallBindings = (componentBody, scopes) => {
|
|
|
45235
44857
|
const callee = declarator.init.callee;
|
|
45236
44858
|
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
45237
44859
|
if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
|
|
45238
|
-
const valueSymbol = scopes.symbolFor(declarator.id);
|
|
45239
|
-
if (!valueSymbol) continue;
|
|
45240
44860
|
bindings.push({
|
|
45241
|
-
|
|
44861
|
+
valueName: declarator.id.name,
|
|
45242
44862
|
hookName: callee.name,
|
|
45243
44863
|
declarator
|
|
45244
44864
|
});
|
|
@@ -45246,49 +44866,6 @@ const findHookCallBindings = (componentBody, scopes) => {
|
|
|
45246
44866
|
}
|
|
45247
44867
|
return bindings;
|
|
45248
44868
|
};
|
|
45249
|
-
const collectExactAliasSymbols = (componentBody, sourceSymbol, scopes) => {
|
|
45250
|
-
const symbols = [sourceSymbol];
|
|
45251
|
-
const symbolIds = new Set([sourceSymbol.id]);
|
|
45252
|
-
const aliasSourceIdentifiers = /* @__PURE__ */ new Set();
|
|
45253
|
-
if (!isNodeOfType(componentBody, "BlockStatement")) return {
|
|
45254
|
-
symbols,
|
|
45255
|
-
aliasSourceIdentifiers
|
|
45256
|
-
};
|
|
45257
|
-
let didFindAlias = true;
|
|
45258
|
-
while (didFindAlias) {
|
|
45259
|
-
didFindAlias = false;
|
|
45260
|
-
for (const statement of componentBody.body ?? []) {
|
|
45261
|
-
if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "const") continue;
|
|
45262
|
-
for (const declarator of statement.declarations ?? []) {
|
|
45263
|
-
let aliasIdentifier = null;
|
|
45264
|
-
let sourceIdentifier = null;
|
|
45265
|
-
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
45266
|
-
const arrayBinding = isNodeOfType(declarator.id, "ArrayPattern") ? declarator.id.elements[0] : null;
|
|
45267
|
-
const arrayValueNode = isNodeOfType(initializer, "ArrayExpression") ? initializer.elements[0] : null;
|
|
45268
|
-
const arrayValue = arrayValueNode && !isNodeOfType(arrayValueNode, "SpreadElement") ? stripParenExpression(arrayValueNode) : null;
|
|
45269
|
-
if (isNodeOfType(declarator.id, "Identifier") && isNodeOfType(initializer, "Identifier")) {
|
|
45270
|
-
aliasIdentifier = declarator.id;
|
|
45271
|
-
sourceIdentifier = initializer;
|
|
45272
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements.length === 1 && isNodeOfType(arrayBinding, "Identifier") && isNodeOfType(initializer, "ArrayExpression") && initializer.elements.length === 1 && isNodeOfType(arrayValue, "Identifier")) {
|
|
45273
|
-
aliasIdentifier = arrayBinding;
|
|
45274
|
-
sourceIdentifier = arrayValue;
|
|
45275
|
-
}
|
|
45276
|
-
if (!aliasIdentifier || !sourceIdentifier) continue;
|
|
45277
|
-
const referencedSymbol = scopes.symbolFor(sourceIdentifier);
|
|
45278
|
-
const aliasSymbol = scopes.symbolFor(aliasIdentifier);
|
|
45279
|
-
if (!referencedSymbol || !symbolIds.has(referencedSymbol.id) || !aliasSymbol || aliasSymbol.kind !== "const" || symbolIds.has(aliasSymbol.id)) continue;
|
|
45280
|
-
symbolIds.add(aliasSymbol.id);
|
|
45281
|
-
symbols.push(aliasSymbol);
|
|
45282
|
-
aliasSourceIdentifiers.add(sourceIdentifier);
|
|
45283
|
-
didFindAlias = true;
|
|
45284
|
-
}
|
|
45285
|
-
}
|
|
45286
|
-
}
|
|
45287
|
-
return {
|
|
45288
|
-
symbols,
|
|
45289
|
-
aliasSourceIdentifiers
|
|
45290
|
-
};
|
|
45291
|
-
};
|
|
45292
44869
|
const rerenderDeferReadsHook = defineRule({
|
|
45293
44870
|
id: "rerender-defer-reads-hook",
|
|
45294
44871
|
title: "URL hook value only read in handlers",
|
|
@@ -45299,13 +44876,15 @@ const rerenderDeferReadsHook = defineRule({
|
|
|
45299
44876
|
create: (context) => {
|
|
45300
44877
|
const checkComponent = (componentBody) => {
|
|
45301
44878
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
45302
|
-
const bindings = findHookCallBindings(componentBody
|
|
44879
|
+
const bindings = findHookCallBindings(componentBody);
|
|
45303
44880
|
if (bindings.length === 0) return;
|
|
45304
44881
|
const handlerBindingNames = collectHandlerBindingNames(componentBody);
|
|
45305
44882
|
for (const binding of bindings) {
|
|
45306
|
-
const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
|
|
45307
44883
|
const referenceLocations = [];
|
|
45308
|
-
|
|
44884
|
+
walkAst(componentBody, (child) => {
|
|
44885
|
+
if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
|
|
44886
|
+
if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
|
|
44887
|
+
});
|
|
45309
44888
|
if (referenceLocations.length === 0) continue;
|
|
45310
44889
|
if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
|
|
45311
44890
|
context.report({
|
|
@@ -45676,10 +45255,14 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45676
45255
|
//#endregion
|
|
45677
45256
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45678
45257
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45679
|
-
const callbackReturnsJsx = (callback
|
|
45258
|
+
const callbackReturnsJsx = (callback) => {
|
|
45680
45259
|
if (!callback) return false;
|
|
45681
45260
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45682
|
-
|
|
45261
|
+
const body = callback.body;
|
|
45262
|
+
if (isJsxExpression(body)) return true;
|
|
45263
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45264
|
+
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45265
|
+
return false;
|
|
45683
45266
|
};
|
|
45684
45267
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45685
45268
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45757,7 +45340,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45757
45340
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45758
45341
|
for (const declarator of stmt.declarations ?? []) {
|
|
45759
45342
|
const init = declarator.init;
|
|
45760
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0]
|
|
45343
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45761
45344
|
memoNode = declarator;
|
|
45762
45345
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45763
45346
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45823,11 +45406,8 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45823
45406
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45824
45407
|
const bindings = /* @__PURE__ */ new Map();
|
|
45825
45408
|
const params = functionNode.params ?? [];
|
|
45826
|
-
for (const
|
|
45827
|
-
|
|
45828
|
-
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45829
|
-
}
|
|
45830
|
-
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45409
|
+
for (const param of params) collectFromObjectPattern(param, bindings);
|
|
45410
|
+
const propsParam = params[0];
|
|
45831
45411
|
const body = functionNode.body;
|
|
45832
45412
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45833
45413
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -46333,8 +45913,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
46333
45913
|
if (statements.length !== 1) return;
|
|
46334
45914
|
const onlyStatement = statements[0];
|
|
46335
45915
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
46336
|
-
singleAssignment =
|
|
46337
|
-
} else if (body) singleAssignment =
|
|
45916
|
+
singleAssignment = onlyStatement.expression;
|
|
45917
|
+
} else if (body) singleAssignment = body;
|
|
46338
45918
|
if (!singleAssignment) return;
|
|
46339
45919
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
46340
45920
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|
|
@@ -53254,8 +52834,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
53254
52834
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
53255
52835
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
53256
52836
|
const firstArg = node.arguments?.[0];
|
|
53257
|
-
if (!
|
|
53258
|
-
if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
|
|
52837
|
+
if (!isNodeOfType(firstArg, "ObjectExpression")) return;
|
|
53259
52838
|
context.report({
|
|
53260
52839
|
node,
|
|
53261
52840
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|
|
@@ -53942,6 +53521,10 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53942
53521
|
}
|
|
53943
53522
|
return false;
|
|
53944
53523
|
};
|
|
53524
|
+
const getJsxOpeningElementName = (node) => {
|
|
53525
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53526
|
+
return null;
|
|
53527
|
+
};
|
|
53945
53528
|
const stylePropObject = defineRule({
|
|
53946
53529
|
id: "style-prop-object",
|
|
53947
53530
|
title: "Style prop is not an object",
|
|
@@ -53953,8 +53536,7 @@ const stylePropObject = defineRule({
|
|
|
53953
53536
|
const allowSet = new Set(allow);
|
|
53954
53537
|
return {
|
|
53955
53538
|
JSXOpeningElement(node) {
|
|
53956
|
-
|
|
53957
|
-
const elementName = resolveJsxElementType(node);
|
|
53539
|
+
const elementName = getJsxOpeningElementName(node);
|
|
53958
53540
|
if (elementName && allowSet.has(elementName)) return;
|
|
53959
53541
|
if (elementName) {
|
|
53960
53542
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54659,7 +54241,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54659
54241
|
create: (context) => {
|
|
54660
54242
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54661
54243
|
return { JSXOpeningElement(node) {
|
|
54662
|
-
if (
|
|
54244
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
54663
54245
|
const attributes = node.attributes ?? [];
|
|
54664
54246
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54665
54247
|
if (!hrefAttribute?.value) return;
|
|
@@ -55280,7 +54862,8 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
55280
54862
|
create: (context) => ({
|
|
55281
54863
|
JSXElement(node) {
|
|
55282
54864
|
const openingElement = node.openingElement;
|
|
55283
|
-
|
|
54865
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
|
|
54866
|
+
const tagName = openingElement.name.name;
|
|
55284
54867
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
55285
54868
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
55286
54869
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|