oxlint-plugin-react-doctor 0.7.6-dev.c6f996e → 0.7.6-dev.dfd47a7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +144 -414
- 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
|
|
@@ -7597,7 +7433,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7597
7433
|
const getOpeningElementTagName = (openingElement) => {
|
|
7598
7434
|
if (!openingElement) return null;
|
|
7599
7435
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7600
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return
|
|
7436
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement.name.name;
|
|
7601
7437
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7602
7438
|
let cursor = openingElement.name;
|
|
7603
7439
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7849,7 +7685,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7849
7685
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7850
7686
|
return { JSXOpeningElement(node) {
|
|
7851
7687
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7852
|
-
const tagName =
|
|
7688
|
+
const tagName = node.name.name;
|
|
7853
7689
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7854
7690
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7855
7691
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -12081,7 +11917,7 @@ const forbidDomProps = defineRule({
|
|
|
12081
11917
|
return { JSXOpeningElement(node) {
|
|
12082
11918
|
if (forbidMap.size === 0) return;
|
|
12083
11919
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12084
|
-
const elementName =
|
|
11920
|
+
const elementName = node.name.name;
|
|
12085
11921
|
if (isReactComponentName(elementName)) return;
|
|
12086
11922
|
for (const attribute of node.attributes) {
|
|
12087
11923
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -12159,7 +11995,7 @@ const forbidElements = defineRule({
|
|
|
12159
11995
|
return {
|
|
12160
11996
|
JSXOpeningElement(node) {
|
|
12161
11997
|
if (forbidMap.size === 0) return;
|
|
12162
|
-
const fullName =
|
|
11998
|
+
const fullName = flattenJsxName$1(node.name);
|
|
12163
11999
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
12164
12000
|
context.report({
|
|
12165
12001
|
node: node.name,
|
|
@@ -12521,7 +12357,8 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12521
12357
|
const isParagraphElement = (candidate) => {
|
|
12522
12358
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12523
12359
|
const opening = candidate.openingElement;
|
|
12524
|
-
|
|
12360
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12361
|
+
return opening.name.name === "p";
|
|
12525
12362
|
};
|
|
12526
12363
|
const findEnclosingParagraph = (openingElement) => {
|
|
12527
12364
|
const owningElement = openingElement.parent;
|
|
@@ -12542,7 +12379,8 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12542
12379
|
severity: "warn",
|
|
12543
12380
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12544
12381
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12545
|
-
|
|
12382
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12383
|
+
const childTagName = node.name.name;
|
|
12546
12384
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12547
12385
|
if (!findEnclosingParagraph(node)) return;
|
|
12548
12386
|
context.report({
|
|
@@ -12573,7 +12411,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12573
12411
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12574
12412
|
const opening = jsxElement.openingElement;
|
|
12575
12413
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12576
|
-
const tagName =
|
|
12414
|
+
const tagName = opening.name.name;
|
|
12577
12415
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12578
12416
|
return tagName;
|
|
12579
12417
|
};
|
|
@@ -12603,7 +12441,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12603
12441
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12604
12442
|
const opening = ancestor.openingElement;
|
|
12605
12443
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12606
|
-
const ancestorTag =
|
|
12444
|
+
const ancestorTag = opening.name.name;
|
|
12607
12445
|
if (ancestorTag.length === 0) {
|
|
12608
12446
|
previous = ancestor;
|
|
12609
12447
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12685,7 +12523,8 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12685
12523
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12686
12524
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12687
12525
|
const opening = candidate.openingElement;
|
|
12688
|
-
|
|
12526
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12527
|
+
return opening.name.name === tagName;
|
|
12689
12528
|
};
|
|
12690
12529
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12691
12530
|
const owningElement = openingElement.parent;
|
|
@@ -12706,7 +12545,8 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12706
12545
|
severity: "warn",
|
|
12707
12546
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12708
12547
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12709
|
-
|
|
12548
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12549
|
+
const tagName = node.name.name;
|
|
12710
12550
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12711
12551
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12712
12552
|
context.report({
|
|
@@ -12821,7 +12661,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12821
12661
|
matchByOccurrence: true,
|
|
12822
12662
|
create: skipNonProductionFiles((context) => ({
|
|
12823
12663
|
JSXOpeningElement(node) {
|
|
12824
|
-
if (
|
|
12664
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
|
|
12825
12665
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12826
12666
|
if (!sandboxAttr) {
|
|
12827
12667
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -13813,28 +13653,6 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13813
13653
|
} })
|
|
13814
13654
|
});
|
|
13815
13655
|
//#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
13656
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13839
13657
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13840
13658
|
"forEach",
|
|
@@ -14019,19 +13837,9 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
14019
13837
|
});
|
|
14020
13838
|
return foundAttachment;
|
|
14021
13839
|
};
|
|
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
13840
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
const elementExpression = assignment.left.object.object;
|
|
13841
|
+
if (!isNodeOfType(styleWriteStatement, "ExpressionStatement") || !isNodeOfType(styleWriteStatement.expression, "AssignmentExpression") || !isNodeOfType(styleWriteStatement.expression.left, "MemberExpression") || !isNodeOfType(styleWriteStatement.expression.left.object, "MemberExpression")) return false;
|
|
13842
|
+
const elementExpression = styleWriteStatement.expression.left.object.object;
|
|
14035
13843
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
14036
13844
|
if (!creationRoot) return false;
|
|
14037
13845
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -14043,17 +13851,15 @@ const jsBatchDomCss = defineRule({
|
|
|
14043
13851
|
severity: "warn",
|
|
14044
13852
|
recommendation: "Do all your reads first, then all your writes. Mixing them inside a loop makes the browser recalculate the layout again and again, which is slow",
|
|
14045
13853
|
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
|
-
};
|
|
13854
|
+
const isStyleAssignment = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.object, "MemberExpression") && isNodeOfType(node.expression.left.object.property, "Identifier") && node.expression.left.object.property.name === "style";
|
|
13855
|
+
const writesLayoutAffectingProperty = (node) => isNodeOfType(node, "ExpressionStatement") && isNodeOfType(node.expression, "AssignmentExpression") && isNodeOfType(node.expression.left, "MemberExpression") && isNodeOfType(node.expression.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(node.expression.left.property.name);
|
|
14050
13856
|
return { BlockStatement(node) {
|
|
14051
13857
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
14052
13858
|
if (!perIterationBody) return;
|
|
14053
13859
|
const statements = node.body ?? [];
|
|
14054
13860
|
let layoutReads = null;
|
|
14055
13861
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
14056
|
-
if (
|
|
13862
|
+
if (!isStyleAssignment(statements[statementIndex]) || !isStyleAssignment(statements[statementIndex - 1])) continue;
|
|
14057
13863
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
14058
13864
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
14059
13865
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -14749,18 +14555,6 @@ const jsHoistRegexp = defineRule({
|
|
|
14749
14555
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14750
14556
|
});
|
|
14751
14557
|
//#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
14558
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14765
14559
|
const referencesParameter = (expression, parameterName) => {
|
|
14766
14560
|
if (!expression) return false;
|
|
@@ -14771,7 +14565,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14771
14565
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14772
14566
|
const callback = node.arguments?.[0];
|
|
14773
14567
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14774
|
-
const firstParameter =
|
|
14568
|
+
const firstParameter = callback.params?.[0];
|
|
14775
14569
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14776
14570
|
let predicate = null;
|
|
14777
14571
|
const body = callback.body;
|
|
@@ -15463,21 +15257,9 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15463
15257
|
};
|
|
15464
15258
|
const getResolvedInitializer = (receiver) => {
|
|
15465
15259
|
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
|
-
};
|
|
15260
|
+
const initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
|
|
15261
|
+
if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
|
|
15262
|
+
return initializer;
|
|
15481
15263
|
};
|
|
15482
15264
|
const isSplitCall = (expression) => {
|
|
15483
15265
|
if (!expression) return false;
|
|
@@ -15643,8 +15425,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15643
15425
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15644
15426
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15645
15427
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15646
|
-
const
|
|
15647
|
-
if (
|
|
15428
|
+
const initializer = getResolvedInitializer(stripped);
|
|
15429
|
+
if (initializer && isSmallInlineLiteralArray(initializer)) return true;
|
|
15648
15430
|
}
|
|
15649
15431
|
return false;
|
|
15650
15432
|
};
|
|
@@ -15696,8 +15478,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15696
15478
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15697
15479
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15698
15480
|
if (resolvedInitializer) {
|
|
15699
|
-
if (isLikelyStringReceiver(resolvedInitializer
|
|
15700
|
-
if (
|
|
15481
|
+
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15482
|
+
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15701
15483
|
}
|
|
15702
15484
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15703
15485
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -16110,21 +15892,14 @@ const jsxFilenameExtension = defineRule({
|
|
|
16110
15892
|
});
|
|
16111
15893
|
//#endregion
|
|
16112
15894
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
16113
|
-
const isJsxFragmentElement = (node
|
|
15895
|
+
const isJsxFragmentElement = (node) => {
|
|
16114
15896
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
16115
15897
|
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
|
-
}
|
|
15898
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) return elementName.name === "Fragment";
|
|
16122
15899
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
16123
15900
|
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);
|
|
15901
|
+
if (elementName.object.name !== "React") return false;
|
|
15902
|
+
return elementName.property.name === "Fragment";
|
|
16128
15903
|
}
|
|
16129
15904
|
return false;
|
|
16130
15905
|
};
|
|
@@ -16150,7 +15925,7 @@ const jsxFragments = defineRule({
|
|
|
16150
15925
|
if (mode !== "syntax") return;
|
|
16151
15926
|
if (!node.closingElement) return;
|
|
16152
15927
|
const openingElement = node.openingElement;
|
|
16153
|
-
if (!isJsxFragmentElement(openingElement
|
|
15928
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
16154
15929
|
if (openingElement.attributes.length > 0) return;
|
|
16155
15930
|
context.report({
|
|
16156
15931
|
node: openingElement,
|
|
@@ -18831,6 +18606,10 @@ const resolveSettings$29 = (settings) => {
|
|
|
18831
18606
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18832
18607
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18833
18608
|
};
|
|
18609
|
+
const getElementName = (node) => {
|
|
18610
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18611
|
+
return null;
|
|
18612
|
+
};
|
|
18834
18613
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18835
18614
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18836
18615
|
const explicit = options.components?.[elementName];
|
|
@@ -18850,8 +18629,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18850
18629
|
create: (context) => {
|
|
18851
18630
|
const options = resolveSettings$29(context.settings);
|
|
18852
18631
|
return { JSXOpeningElement(node) {
|
|
18853
|
-
|
|
18854
|
-
const elementName = resolveJsxElementType(node);
|
|
18632
|
+
const elementName = getElementName(node);
|
|
18855
18633
|
if (!elementName) return;
|
|
18856
18634
|
for (const attribute of node.attributes) {
|
|
18857
18635
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -19038,6 +18816,11 @@ const checkTarget = (attributeValue) => {
|
|
|
19038
18816
|
alternate: false
|
|
19039
18817
|
};
|
|
19040
18818
|
};
|
|
18819
|
+
const getOpeningElementName = (node) => {
|
|
18820
|
+
const name = node.name;
|
|
18821
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18822
|
+
return null;
|
|
18823
|
+
};
|
|
19041
18824
|
const jsxNoTargetBlank = defineRule({
|
|
19042
18825
|
id: "jsx-no-target-blank",
|
|
19043
18826
|
title: "Unsafe target=_blank link",
|
|
@@ -19057,8 +18840,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
19057
18840
|
return settings.formComponents.has(tagName);
|
|
19058
18841
|
};
|
|
19059
18842
|
return { JSXOpeningElement(node) {
|
|
19060
|
-
|
|
19061
|
-
const tagName = resolveJsxElementType(node);
|
|
18843
|
+
const tagName = getOpeningElementName(node);
|
|
19062
18844
|
if (!tagName) return;
|
|
19063
18845
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
19064
18846
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19291,7 +19073,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19291
19073
|
return {
|
|
19292
19074
|
JSXElement(node) {
|
|
19293
19075
|
const openingElement = node.openingElement;
|
|
19294
|
-
if (!isJsxFragmentElement(openingElement
|
|
19076
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
19295
19077
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19296
19078
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19297
19079
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20533,6 +20315,9 @@ const hasDirective = (programNode, directive) => {
|
|
|
20533
20315
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20534
20316
|
};
|
|
20535
20317
|
//#endregion
|
|
20318
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20319
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20320
|
+
//#endregion
|
|
20536
20321
|
//#region src/plugin/utils/is-component-assignment.ts
|
|
20537
20322
|
const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
20538
20323
|
//#endregion
|
|
@@ -20865,7 +20650,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20865
20650
|
severity: "warn",
|
|
20866
20651
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20867
20652
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20868
|
-
if (
|
|
20653
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
20869
20654
|
const attributes = node.attributes ?? [];
|
|
20870
20655
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20871
20656
|
if (downloadAttribute) {
|
|
@@ -21061,7 +20846,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
21061
20846
|
severity: "warn",
|
|
21062
20847
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
21063
20848
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21064
|
-
if (
|
|
20849
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21065
20850
|
const attributes = node.attributes ?? [];
|
|
21066
20851
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
21067
20852
|
if (!relAttribute?.value) return;
|
|
@@ -21169,7 +20954,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21169
20954
|
severity: "warn",
|
|
21170
20955
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21171
20956
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21172
|
-
if (
|
|
20957
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21173
20958
|
const attributes = node.attributes ?? [];
|
|
21174
20959
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21175
20960
|
if (!hrefAttribute?.value) return;
|
|
@@ -21344,7 +21129,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21344
21129
|
create: (context) => {
|
|
21345
21130
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21346
21131
|
return { JSXOpeningElement(node) {
|
|
21347
|
-
if (
|
|
21132
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
21348
21133
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21349
21134
|
const programRoot = findProgramRoot(node);
|
|
21350
21135
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21391,8 +21176,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21391
21176
|
severity: "warn",
|
|
21392
21177
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21393
21178
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21394
|
-
|
|
21395
|
-
if (
|
|
21179
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
21180
|
+
if (node.name.name !== "script" && node.name.name !== "Script") return;
|
|
21396
21181
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21397
21182
|
if (!srcAttribute?.value) return;
|
|
21398
21183
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23200,15 +22985,10 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23200
22985
|
if (!declaringNode) return false;
|
|
23201
22986
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23202
22987
|
}));
|
|
23203
|
-
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23204
|
-
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23205
|
-
let declaringFunction = bindingNode.parent;
|
|
23206
|
-
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23207
|
-
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23208
|
-
};
|
|
23209
22988
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23210
22989
|
if (def.type !== "Parameter") return false;
|
|
23211
|
-
|
|
22990
|
+
const bindingParent = def.name.parent;
|
|
22991
|
+
return isFunctionLike$1(bindingParent);
|
|
23212
22992
|
}));
|
|
23213
22993
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23214
22994
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26205,56 +25985,6 @@ const noBarrelImport = defineRule({
|
|
|
26205
25985
|
}
|
|
26206
25986
|
});
|
|
26207
25987
|
//#endregion
|
|
26208
|
-
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26209
|
-
const collectReturnedExpressions = (functionNode) => {
|
|
26210
|
-
if (!isFunctionLike$1(functionNode)) return [];
|
|
26211
|
-
const body = functionNode.body;
|
|
26212
|
-
if (!body) return [];
|
|
26213
|
-
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26214
|
-
const returnedExpressions = [];
|
|
26215
|
-
walkAst(body, (node) => {
|
|
26216
|
-
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26217
|
-
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26218
|
-
});
|
|
26219
|
-
return returnedExpressions;
|
|
26220
|
-
};
|
|
26221
|
-
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26222
|
-
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26223
|
-
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26224
|
-
const functionMatches = (candidateFunction) => {
|
|
26225
|
-
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26226
|
-
visitedFunctions.add(candidateFunction);
|
|
26227
|
-
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26228
|
-
};
|
|
26229
|
-
const expressionMatches = (expression) => {
|
|
26230
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
26231
|
-
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26232
|
-
visitedExpressions.add(unwrappedExpression);
|
|
26233
|
-
if (matchesExpression(unwrappedExpression)) return true;
|
|
26234
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26235
|
-
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26236
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26237
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
26238
|
-
if (isFunctionLike$1(initializer)) return false;
|
|
26239
|
-
return expressionMatches(initializer);
|
|
26240
|
-
}
|
|
26241
|
-
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26242
|
-
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26243
|
-
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26244
|
-
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26245
|
-
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26246
|
-
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26247
|
-
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26248
|
-
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26249
|
-
return functionMatches(candidateFunction);
|
|
26250
|
-
}
|
|
26251
|
-
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26252
|
-
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26253
|
-
return false;
|
|
26254
|
-
};
|
|
26255
|
-
return functionMatches(functionNode);
|
|
26256
|
-
};
|
|
26257
|
-
//#endregion
|
|
26258
25988
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26259
25989
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26260
25990
|
"FunctionDeclaration",
|
|
@@ -26274,13 +26004,12 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26274
26004
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26275
26005
|
};
|
|
26276
26006
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26277
|
-
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
26278
26007
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26279
26008
|
let hasRenderOutput = false;
|
|
26280
26009
|
walkAst(rootNode, (node) => {
|
|
26281
26010
|
if (hasRenderOutput) return false;
|
|
26282
26011
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26283
|
-
if (
|
|
26012
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
|
|
26284
26013
|
hasRenderOutput = true;
|
|
26285
26014
|
return false;
|
|
26286
26015
|
}
|
|
@@ -26291,7 +26020,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26291
26020
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26292
26021
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26293
26022
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26294
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes)
|
|
26023
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26295
26024
|
renderOutputCache.set(functionNode, {
|
|
26296
26025
|
scopes,
|
|
26297
26026
|
hasRenderOutput
|
|
@@ -28756,7 +28485,7 @@ const noDisabledZoom = defineRule({
|
|
|
28756
28485
|
category: "Accessibility",
|
|
28757
28486
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28758
28487
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28759
|
-
if (
|
|
28488
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
|
|
28760
28489
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28761
28490
|
if (!nameAttr?.value) return;
|
|
28762
28491
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -29146,7 +28875,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
29146
28875
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
29147
28876
|
for (const statement of componentBody.body ?? []) {
|
|
29148
28877
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
29149
|
-
const expression =
|
|
28878
|
+
const expression = statement.expression;
|
|
29150
28879
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
29151
28880
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
29152
28881
|
effectCalls.push(expression);
|
|
@@ -31928,7 +31657,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31928
31657
|
severity: "warn",
|
|
31929
31658
|
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
31930
31659
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31931
|
-
if (
|
|
31660
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
31932
31661
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31933
31662
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31934
31663
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -32169,7 +31898,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
32169
31898
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
32170
31899
|
return {
|
|
32171
31900
|
JSXOpeningElement(node) {
|
|
32172
|
-
if (
|
|
31901
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
32173
31902
|
let typeAttribute = null;
|
|
32174
31903
|
let typeAttributeIndex = null;
|
|
32175
31904
|
let indeterminateAttribute = null;
|
|
@@ -33241,13 +32970,11 @@ const noManyBooleanProps = defineRule({
|
|
|
33241
32970
|
};
|
|
33242
32971
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33243
32972
|
if (!param) return;
|
|
33244
|
-
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33245
|
-
if (!propsBinding) return;
|
|
33246
32973
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33247
|
-
if (isNodeOfType(
|
|
32974
|
+
if (isNodeOfType(param, "ObjectPattern")) {
|
|
33248
32975
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33249
32976
|
const booleanLikePropNames = [];
|
|
33250
|
-
for (const property of
|
|
32977
|
+
for (const property of param.properties ?? []) {
|
|
33251
32978
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33252
32979
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33253
32980
|
if (!keyName) continue;
|
|
@@ -33258,7 +32985,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33258
32985
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33259
32986
|
return;
|
|
33260
32987
|
}
|
|
33261
|
-
if (isNodeOfType(
|
|
32988
|
+
if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
|
|
33262
32989
|
};
|
|
33263
32990
|
return {
|
|
33264
32991
|
FunctionDeclaration(node) {
|
|
@@ -33380,7 +33107,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33380
33107
|
if (mirrorBindings.length === 0) return;
|
|
33381
33108
|
for (const statement of componentBody.body ?? []) {
|
|
33382
33109
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
33383
|
-
const effectCall =
|
|
33110
|
+
const effectCall = statement.expression;
|
|
33384
33111
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
33385
33112
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
33386
33113
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -33394,7 +33121,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33394
33121
|
const bodyStatements = getCallbackStatements(callback);
|
|
33395
33122
|
if (bodyStatements.length !== 1) continue;
|
|
33396
33123
|
const onlyStatement = bodyStatements[0];
|
|
33397
|
-
const expression =
|
|
33124
|
+
const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
|
|
33398
33125
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
33399
33126
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
33400
33127
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -36010,7 +35737,7 @@ const noPreventDefault = defineRule({
|
|
|
36010
35737
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
36011
35738
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
36012
35739
|
return { JSXOpeningElement(node) {
|
|
36013
|
-
const elementName =
|
|
35740
|
+
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
36014
35741
|
if (!elementName) return;
|
|
36015
35742
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
36016
35743
|
if (!targetEventProps) return;
|
|
@@ -37495,7 +37222,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37495
37222
|
return expressionReadsStateValue(argument, stateName);
|
|
37496
37223
|
};
|
|
37497
37224
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37498
|
-
const expression =
|
|
37225
|
+
const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
|
|
37499
37226
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37500
37227
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37501
37228
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -37844,7 +37571,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
37844
37571
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
37845
37572
|
for (const statement of functionBody.body ?? []) {
|
|
37846
37573
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37847
|
-
const effectCall =
|
|
37574
|
+
const effectCall = statement.expression;
|
|
37848
37575
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
37849
37576
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
37850
37577
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -38474,10 +38201,9 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38474
38201
|
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
38475
38202
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38476
38203
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38477
|
-
const
|
|
38478
|
-
const firstCharacter = elementName.charCodeAt(0);
|
|
38204
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
38479
38205
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38480
|
-
if (
|
|
38206
|
+
if (node.name.name.includes("-")) return;
|
|
38481
38207
|
for (const attribute of node.attributes) {
|
|
38482
38208
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38483
38209
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38865,7 +38591,8 @@ const noUncontrolledInput = defineRule({
|
|
|
38865
38591
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38866
38592
|
walkAst(componentBody, (child) => {
|
|
38867
38593
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38868
|
-
|
|
38594
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
38595
|
+
const tagName = child.name.name;
|
|
38869
38596
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38870
38597
|
const attributes = child.attributes ?? [];
|
|
38871
38598
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38926,7 +38653,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38926
38653
|
severity: "warn",
|
|
38927
38654
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38928
38655
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38929
|
-
if (
|
|
38656
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
|
|
38930
38657
|
const attributes = node.attributes ?? [];
|
|
38931
38658
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38932
38659
|
if (!srcAttribute) return;
|
|
@@ -40141,7 +39868,7 @@ const noUnknownProperty = defineRule({
|
|
|
40141
39868
|
}
|
|
40142
39869
|
if (fileIsNonReactJsx) return;
|
|
40143
39870
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
40144
|
-
const elementType =
|
|
39871
|
+
const elementType = node.name.name;
|
|
40145
39872
|
const firstCharacter = elementType.charCodeAt(0);
|
|
40146
39873
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
40147
39874
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40319,21 +40046,20 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40319
40046
|
});
|
|
40320
40047
|
return found;
|
|
40321
40048
|
};
|
|
40322
|
-
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40323
40049
|
const isReactClassComponent = (classNode) => {
|
|
40324
40050
|
if (isEs6Component(classNode)) return true;
|
|
40325
40051
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40326
40052
|
};
|
|
40327
|
-
const findEnclosingComponent = (node
|
|
40053
|
+
const findEnclosingComponent = (node) => {
|
|
40328
40054
|
let walker = node.parent;
|
|
40329
40055
|
while (walker) {
|
|
40330
40056
|
if (isFunctionLike$1(walker)) {
|
|
40331
40057
|
const componentName = inferFunctionLikeName(walker);
|
|
40332
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40058
|
+
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
40333
40059
|
component: walker,
|
|
40334
40060
|
name: componentName
|
|
40335
40061
|
};
|
|
40336
|
-
if (!componentName &&
|
|
40062
|
+
if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40337
40063
|
component: walker,
|
|
40338
40064
|
name: null
|
|
40339
40065
|
};
|
|
@@ -40572,7 +40298,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40572
40298
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40573
40299
|
if (settings.allowAsProps) return;
|
|
40574
40300
|
}
|
|
40575
|
-
const enclosing = findEnclosingComponent(candidateNode
|
|
40301
|
+
const enclosing = findEnclosingComponent(candidateNode);
|
|
40576
40302
|
if (!enclosing) return;
|
|
40577
40303
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40578
40304
|
queuedReports.push({
|
|
@@ -40583,7 +40309,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40583
40309
|
});
|
|
40584
40310
|
};
|
|
40585
40311
|
const checkFunctionLike = (node) => {
|
|
40586
|
-
if (!
|
|
40312
|
+
if (!expressionContainsJsxOrCreateElement(node)) return;
|
|
40587
40313
|
const inferredName = inferFunctionLikeName(node);
|
|
40588
40314
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40589
40315
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41843,7 +41569,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41843
41569
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41844
41570
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41845
41571
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41846
|
-
const tagName =
|
|
41572
|
+
const tagName = node.name.name;
|
|
41847
41573
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41848
41574
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41849
41575
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41863,7 +41589,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41863
41589
|
]);
|
|
41864
41590
|
const isTextLikeInput = (openingElement) => {
|
|
41865
41591
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41866
|
-
const tagName =
|
|
41592
|
+
const tagName = openingElement.name.name;
|
|
41867
41593
|
if (tagName === "textarea") return true;
|
|
41868
41594
|
if (tagName !== "input") return false;
|
|
41869
41595
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -42020,9 +41746,8 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
42020
41746
|
]);
|
|
42021
41747
|
const collectBooleanPropBindings = (param) => {
|
|
42022
41748
|
const bindings = /* @__PURE__ */ new Set();
|
|
42023
|
-
|
|
42024
|
-
|
|
42025
|
-
for (const property of propsBinding.properties ?? []) {
|
|
41749
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
41750
|
+
for (const property of param.properties ?? []) {
|
|
42026
41751
|
if (!isNodeOfType(property, "Property")) continue;
|
|
42027
41752
|
if (property.computed) continue;
|
|
42028
41753
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42283,7 +42008,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42283
42008
|
},
|
|
42284
42009
|
JSXOpeningElement(node) {
|
|
42285
42010
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42286
|
-
const tagName =
|
|
42011
|
+
const tagName = node.name.name;
|
|
42287
42012
|
if (tagName === "dialog") return;
|
|
42288
42013
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42289
42014
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43830,13 +43555,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43830
43555
|
}
|
|
43831
43556
|
return null;
|
|
43832
43557
|
};
|
|
43833
|
-
const resolveHookNameFromInitializer = (initializer
|
|
43558
|
+
const resolveHookNameFromInitializer = (initializer) => {
|
|
43834
43559
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43835
43560
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43836
|
-
const
|
|
43837
|
-
if (
|
|
43838
|
-
if (!isNodeOfType(
|
|
43839
|
-
return resolveTanstackQueryHookName(
|
|
43561
|
+
const binding = findVariableInitializer(initializer, initializer.name);
|
|
43562
|
+
if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
|
|
43563
|
+
if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
43564
|
+
return resolveTanstackQueryHookName(binding.initializer);
|
|
43840
43565
|
};
|
|
43841
43566
|
const queryNoRestDestructuring = defineRule({
|
|
43842
43567
|
id: "query-no-rest-destructuring",
|
|
@@ -43849,7 +43574,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43849
43574
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43850
43575
|
if (!node.init) return;
|
|
43851
43576
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43852
|
-
const hookName = resolveHookNameFromInitializer(node.init
|
|
43577
|
+
const hookName = resolveHookNameFromInitializer(node.init);
|
|
43853
43578
|
if (!hookName) return;
|
|
43854
43579
|
context.report({
|
|
43855
43580
|
node: node.id,
|
|
@@ -44320,7 +44045,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44320
44045
|
severity: "warn",
|
|
44321
44046
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44322
44047
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44323
|
-
if (
|
|
44048
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
44324
44049
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44325
44050
|
node,
|
|
44326
44051
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45613,10 +45338,14 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45613
45338
|
//#endregion
|
|
45614
45339
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45615
45340
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45616
|
-
const callbackReturnsJsx = (callback
|
|
45341
|
+
const callbackReturnsJsx = (callback) => {
|
|
45617
45342
|
if (!callback) return false;
|
|
45618
45343
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45619
|
-
|
|
45344
|
+
const body = callback.body;
|
|
45345
|
+
if (isJsxExpression(body)) return true;
|
|
45346
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45347
|
+
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45348
|
+
return false;
|
|
45620
45349
|
};
|
|
45621
45350
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45622
45351
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45694,7 +45423,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45694
45423
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45695
45424
|
for (const declarator of stmt.declarations ?? []) {
|
|
45696
45425
|
const init = declarator.init;
|
|
45697
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0]
|
|
45426
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45698
45427
|
memoNode = declarator;
|
|
45699
45428
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45700
45429
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45760,11 +45489,8 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45760
45489
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45761
45490
|
const bindings = /* @__PURE__ */ new Map();
|
|
45762
45491
|
const params = functionNode.params ?? [];
|
|
45763
|
-
for (const
|
|
45764
|
-
|
|
45765
|
-
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45766
|
-
}
|
|
45767
|
-
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45492
|
+
for (const param of params) collectFromObjectPattern(param, bindings);
|
|
45493
|
+
const propsParam = params[0];
|
|
45768
45494
|
const body = functionNode.body;
|
|
45769
45495
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45770
45496
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -46270,8 +45996,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
46270
45996
|
if (statements.length !== 1) return;
|
|
46271
45997
|
const onlyStatement = statements[0];
|
|
46272
45998
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
46273
|
-
singleAssignment =
|
|
46274
|
-
} else if (body) singleAssignment =
|
|
45999
|
+
singleAssignment = onlyStatement.expression;
|
|
46000
|
+
} else if (body) singleAssignment = body;
|
|
46275
46001
|
if (!singleAssignment) return;
|
|
46276
46002
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
46277
46003
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|
|
@@ -53878,6 +53604,10 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53878
53604
|
}
|
|
53879
53605
|
return false;
|
|
53880
53606
|
};
|
|
53607
|
+
const getJsxOpeningElementName = (node) => {
|
|
53608
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53609
|
+
return null;
|
|
53610
|
+
};
|
|
53881
53611
|
const stylePropObject = defineRule({
|
|
53882
53612
|
id: "style-prop-object",
|
|
53883
53613
|
title: "Style prop is not an object",
|
|
@@ -53889,8 +53619,7 @@ const stylePropObject = defineRule({
|
|
|
53889
53619
|
const allowSet = new Set(allow);
|
|
53890
53620
|
return {
|
|
53891
53621
|
JSXOpeningElement(node) {
|
|
53892
|
-
|
|
53893
|
-
const elementName = resolveJsxElementType(node);
|
|
53622
|
+
const elementName = getJsxOpeningElementName(node);
|
|
53894
53623
|
if (elementName && allowSet.has(elementName)) return;
|
|
53895
53624
|
if (elementName) {
|
|
53896
53625
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54595,7 +54324,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54595
54324
|
create: (context) => {
|
|
54596
54325
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54597
54326
|
return { JSXOpeningElement(node) {
|
|
54598
|
-
if (
|
|
54327
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
54599
54328
|
const attributes = node.attributes ?? [];
|
|
54600
54329
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54601
54330
|
if (!hrefAttribute?.value) return;
|
|
@@ -55216,7 +54945,8 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
55216
54945
|
create: (context) => ({
|
|
55217
54946
|
JSXElement(node) {
|
|
55218
54947
|
const openingElement = node.openingElement;
|
|
55219
|
-
|
|
54948
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
|
|
54949
|
+
const tagName = openingElement.name.name;
|
|
55220
54950
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
55221
54951
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
55222
54952
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|